在 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
传递给此方法以显示对话框,如下面代码:
1 | import 'package:flutter/material.dart'; |
如何使用 BuildContext
通常我们在使用 BuildContext
前会通过 State
的属性 mounted
来判断再使用,这是因为 State
是依附于 Element
创建,Element
的生命周期和 State
是同步的。如果 Element
销毁了,那此时的 mounted
则为 false
,再去使用 BuildContext
就会报错,为 true
才可以继续使用,代码如下:
1 | TextButton( |
在逻辑层使用 BuildContext
有时候我们在 ViewModel
或者 Bloc
异步执行完成一些操作后,再使用 BuildContext
返回页面或者弹出提示框,如下面的代码:
1 | TextButton( |
而此时的 ViewModel
或者 Bloc
没有 BuildContext
,同时,如上面代码需要在 UI
展示层来处理与功能相关的逻辑,随着 App
的需求和功能的扩展,有可能会在这里添加更多逻辑,造成视图层和逻辑层代码耦合在一起,不好维护。那要在 ViewModel
或者 Bloc
使用 BuildContext
该如何做呢?
- 创建类
NavigationService
,并给添加一个GlobalKey
属性。1
2
3
4
5
6
7
8
9
10class 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
容器中。1
2
3
4GetIt locator = GetIt.instance;
void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
} - 将
navigatorKey
赋值给程序入口Widget
的key
,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class App extends StatelessWidget {
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
中的代码,异步操作完成后跳转页面。1
2
3
4
5
6
7
8
9
10
11
12class 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层调用,不再写逻辑判断了。 这样达到了
1
2
3
4
5
6TextButton(
onPressed: () async {
await model.login(success: true);
},
child: const Text('Close'),
),ViewModel
层处理所有逻辑,视图应该只调用模型上的函数,然后在需要时使用新状态rebild
或者其它操作,降低了彼此之间的耦合。
需要注意什么?
- 作用域问题,确保使用的
BuildContext
在正确的作用域内,即所在的Widget Tree
中。避免在Widget Tree
之外的地方使用BuildContext
,否则可能导致运行时错误. - 生命周期问题,
BuildContext
的生命周期与相应的Widget
相关联。当Widget
被创建时,会创建一个新的BuildContext
对象,并在Widget
树中传递。当Widget
被移除时,相关的BuildContext
也会被销毁。因此,在保存BuildContext
时,要确保它的生命周期与所需的操作相匹配,避免出现空指针异常。 - 尽量避免在
build()
函数中利用BuildContext
获取MediaQuery
的size
和padding
做大量计算的操作,如下面代码:上面这种用法可能会导致键盘在弹出的时候,虽然当前页面并没有完全展示,但是也会导致你的控件不断重新计算从而出现卡顿。1
2
3
4
5
6
7
8
9
10
11
12
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,
);
}
好了,今天分享就到这里,感谢您的阅读,记得关注加点赞哦。