深化了解 Flutter 中的 BuildContext

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 传递给此方法以显现对话框,如下面代码:

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 该怎么做呢?

  1. 创立类 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();
      }
    }
    
  2. NavigationService 注册到 get_it 容器中。

    GetIt locator = GetIt.instance;
    void setupLocator() {
      locator.registerLazySingleton(() => NavigationService());
    }
    
  3. navigatorKey 赋值给程序入口 Widgetkey

    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 中的代码,异步操作完成后跳转页面。

    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层调用。

    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 做大量核算的操作,如下面代码:

    @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小技巧之优化运用的 BuildContext,里边有比较具体介绍。

好了,今日共享就到这儿,感谢您的阅读,记得重视加点赞哦。