BuildContext 是什么
在 Flutter
中 BuildContext
可太常见了,不管是 StatelessWidget
还是 StatefulWidget
的 build()
函数参数都会带有 BuildContext
,好像随处可见,就像我们的一位老朋友,但似乎又对其知之甚少(熟悉的陌生人),今天我们再来了解一下这位老朋友 BuildContext
,看看它在 Flutter
架构中扮演什么角色,我们该如何使用它及使用的时候需要注意什么。
BuildContext
是什么
打开 BuildContext
所在的文档的看到的第一句话就是 A handle to the location of a widget in the widget tree.
(翻译过来:小部件树中小部件位置的句柄),啥意思呢?
每一个 Widget
都有自己的 BuildContext
,而 BuildContext
代表了 Widget
在 Widget Tree
中的位置,常用于在 Widget Tree
中查找和定位 Widget
,或者执行任务,例如导航到其他屏幕、显示对话框、访问主题数据等,如 Theme.of(context)
、Navigator.of(context)
。
BuildContext
提供对 Widget
和资源的访问,以及对当前 Widget
最近的祖先Widget
的其他数据的访问。 如每个 Widget
的 build()
函数中使用的 BuildContext
参数,就是 Flutter
框架通过 Widget Tree
向下传递的 BuildContext
。
假设现在显示一个对话框。即使用 showDialog()
方法创建对话框,但同时 showDialog()
需要传一个 BuildContext
参数。此时就可以把当前 Widget
的 BuildContext
传递给此方法以显示对话框,如下面代码:
import 'package:flutter/material.dart';
class BuildContextPage extends StatefulWidget {
const BuildContextPage({Key? key}) : super(key: key);
@override
State<BuildContextPage> createState() => _BuildContextPageState();
}
class _BuildContextPageState extends State<BuildContextPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffffffff),
body: Center(
child: Column(
children: [
TextButton(
child: const Text('ShowAlert'),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Dialog Title'),
content: const Text('This is the content of the dialog.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
);
},
);
},
),
],
)));
}
}
如何使用 BuildContext
通常我们在使用 BuildContext
前会通过 State
的属性 mounted
来判断再使用,这是因为 State
是依附于 Element
创建,Element
的生命周期和 State
是同步的。如果 Element
销毁了,那此时的 mounted
则为 false
,再去使用 BuildContext
就会报错,为 true
才可以继续使用,代码如下:
TextButton(
onPressed: () async {
await Future.delayed(const Duration(seconds: 3));
if (!mounted) return;
Navigator.of(context).pop();
},
child: const Text('Close'),
)
在逻辑层使用 BuildContext
有时候我们在 ViewModel
或者 Bloc
异步执行完成一些操作后,再使用 BuildContext
返回页面或者弹出提示框,如下面的代码:
TextButton(
onPressed: () async {
var success = await model.login(success: true);
if (success) {
Navigator.of(context).pushNamed("");
}
},
child: const Text('Close'),
),
而此时的 ViewModel
或者 Bloc
没有 BuildContext
,同时,如上面代码需要在 UI
展示层来处理与功能相关的逻辑,随着 App
的需求和功能的扩展,有可能会在这里添加更多逻辑,造成视图层和逻辑层代码耦合在一起,不好维护。那要在 ViewModel
或者 Bloc
使用 BuildContext
该如何做呢?
创建类 NavigationService
,并给添加一个 GlobalKey
属性。
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Future<dynamic>? navigateTo(String routeName) {
return navigatorKey.currentState?.pushNamed(routeName);
}
void goBack() {
return navigatorKey.currentState?.pop();
}
}
将 NavigationService
注册到 get_it
容器中。
GetIt locator = GetIt.instance;
void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
}
将 navigatorKey
赋值给程序入口 Widget
的 key
。
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) {
return AppLanguageProvider();
}),
],
builder: (BuildContext context, Widget? child) {
return MaterialApp(
...
key: locator<NavigationService>().navigatorKey,
onGenerateRoute: MyRoutes.router.generator,
initialRoute: MyRoutes.root,
...,
);
},
);
}
}
修改 LoginViewModel
中的代码,异步操作完成后跳转页面。
class LoginViewModel extends ChangeNotifier {
final NavigationService _navigationService = locator<NavigationService>();
Future<bool> login({bool success = true}) async {
/// 模拟网络请求
await Future.delayed(const Duration(seconds: 1));
if (success) {
_navigationService.navigateTo("");
return true;
}
return false;
}
}
页面 UI 层调用,不再写逻辑判断了。
TextButton(
onPressed: () async {
await model.login(success: true);
},
child: const Text('Close'),
),
这样达到了 ViewModel
层处理所有逻辑,视图应该只调用模型上的函数,然后在需要时使用新状态 rebuild
或者其它操作,降低了彼此之间的耦合。
需要注意什么?
- 作用域问题,确保使用的
BuildContext
在正确的作用域内,即所在的Widget Tree
中。避免在Widget Tree
之外的地方使用BuildContext
,否则可能导致运行时错误。 - 生命周期问题,
BuildContext
的生命周期与相应的Widget
相关联。当Widget
被创建时,会创建一个新的BuildContext
对象,并在Widget
树中传递。当Widget
被移除时,相关的BuildContext
也会被销毁。因此,在保存BuildContext
时,要确保它的生命周期与所需的操作相匹配,避免出现空指针异常。 - 尽量避免在
build()
函数中利用BuildContext
获取MediaQuery
的size
和padding
做大量计算的操作,如下面代码:
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var padding = MediaQuery.of(context).padding;
var width = size.width / 2;
var height = size.width / size.height * (40 - padding.bottom);
return Container(
color: Colors.amber,
width: width,
height: height,
);
}
上面这种用法可能会导致键盘在弹出的时候,虽然当前页面并没有完全展示,但是也会导致你的控件不断重新计算从而出现卡顿。
好了,今天分享就到这里,更多干货文章请关注公众号:Flutter技术实践,感谢您的阅读,记得关注加点赞哦。