0%

深入了解 Flutter 中的 BuildContext

19.png

FlutterBuildContext 可太常见了,不管是 StatelessWidget 还是 StatefulWidgetbuild() 函数参数都会带有 BuildContext,好像随处可见,就像我们的一位老朋友,但似乎又对其知之甚少(熟悉的陌生人),今天我们再来了解一下这位老朋友 BuildContext,看看它在 Flutter 架构中扮演什么角色,我们该如何使用它及使用的时候需要注意什么。

BuildContext 是什么

打开 BuildContext 所在的文档的看到的第一句话就是 A handle to the location of a widget in the widget tree. (翻译过来:小部件树中小部件位置的句柄),啥意思呢?

每一个 Widget 都有自己的 BuildContext,而 BuildContext 代表了 WidgetWidget Tree 中的位置,常用于在 Widget Tree 中查找和定位 Widget,或者执行任务,例如导航到其他屏幕、显示对话框、访问主题数据等,如 Theme.of(context)Navigator.of(context)

BuildContext 提供对 Widget 和资源的访问,以及对当前 Widget 最近的祖先Widget的其他数据的访问。 如每个 Widgetbuild() 函数中使用的 BuildContext 参数,就是 Flutter 框架通过 Widget Tree 向下传递的 BuildContext

假设现在显示一个对话框。即使用 showDialog() 方法创建对话框,但同时 showDialog() 需要传一个 BuildContext 参数。此时就可以把当前 WidgetBuildContext 传递给此方法以显示对话框,如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 才可以继续使用,代码如下:

1
2
3
4
5
6
7
8
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 返回页面或者弹出提示框,如下面的代码:

1
2
3
4
5
6
7
8
9
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 该如何做呢?

  1. 创建类 NavigationService ,并给添加一个 GlobalKey 属性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class NavigationService {
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    Future<dynamic>? navigateTo(String routeName) {
    return navigatorKey.currentState?.pushNamed(routeName);
    }

    void goBack() {
    return navigatorKey.currentState?.pop();
    }
    }
  2. NavigationService 注册到 get_it 容器中。
    1
    2
    3
    4
    GetIt locator = GetIt.instance;
    void setupLocator() {
    locator.registerLazySingleton(() => NavigationService());
    }
  3. navigatorKey 赋值给程序入口 Widgetkey
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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,
    ...,
    );
    },
    );
    }
    }
  4. 修改 LoginViewModel 中的代码,异步操作完成后跳转页面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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;
    }
    }
  5. 页面UI层调用,不再写逻辑判断了。
    1
    2
    3
    4
    5
    6
    TextButton(
    onPressed: () async {
    await model.login(success: true);
    },
    child: const Text('Close'),
    ),
    这样达到了 ViewModel 层处理所有逻辑,视图应该只调用模型上的函数,然后在需要时使用新状态 rebild 或者其它操作,降低了彼此之间的耦合。

需要注意什么?

  1. 作用域问题,确保使用的 BuildContext 在正确的作用域内,即所在的 Widget Tree 中。避免在 Widget Tree 之外的地方使用 BuildContext,否则可能导致运行时错误.
  2. 生命周期问题,BuildContext 的生命周期与相应的 Widget 相关联。当 Widget 被创建时,会创建一个新的 BuildContext 对象,并在 Widget 树中传递。当 Widget 被移除时,相关的 BuildContext 也会被销毁。因此,在保存BuildContext 时,要确保它的生命周期与所需的操作相匹配,避免出现空指针异常。
  3. 尽量避免在 build() 函数中利用 BuildContext 获取 MediaQuerysizepadding 做大量计算的操作,如下面代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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技术实践