深入了解 Flutter 中的 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 传递给此方法以显示对话框,如下面代码:

Dart 复制代码
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 才可以继续使用,代码如下:

Dart 复制代码
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 返回页面或者弹出提示框,如下面的代码:

Dart 复制代码
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 属性。

    Dart 复制代码
    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 容器中。

    Dart 复制代码
    GetIt locator = GetIt.instance;
    void setupLocator() {
      locator.registerLazySingleton(() => NavigationService());
    }
  3. 将 navigatorKey 赋值给程序入口 Widget 的 key。

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

    Dart 复制代码
    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层调用。

    Dart 复制代码
    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 获取 MediaQuery 的 size 和 padding 做大量计算的操作,如下面代码:

    Dart 复制代码
    @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,里面有比较详细介绍。

好了,今天分享就到这里,感谢您的阅读,记得关注加点赞哦。

相关推荐
2601_949970699 小时前
HumanizeDuration 跨端三方库仓颉适配全流程
前端·javascript·flutter
独隅10 小时前
Claude API 实战教程:从入门到完整项目
android·java·数据库·claude·claude code
传奇开心果编程11 小时前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
flutter·移动开发·跨平台·dart·编程语言理论·编程框架·声明式ui
程序员老刘13 小时前
你的Flutter项目要不要紧急升级3.47.8?
flutter·客户端
且随疾风前行.14 小时前
Paint Cavans高级绘制:[一]Paint画笔高级应用
android·ui
Talyra4215 小时前
不想开会员,我写了一个记账 App
flutter
VicLi15 小时前
把常用 iOS 调试操作放进一个 macOS 控制台: iCATBox
ios
郑州光合科技余经理15 小时前
同城小程序开发:同一笔单在用户端和商家端各显示到哪
android·java·开发语言·前端·后端·uni-app·php
茶底世界之下16 小时前
雾景需要更强反差吗?用 Awaking 检查树影与雾气的平衡
ios
pigdreams17 小时前
Google 官方 Android CLI:让AI直接读布局结构而不是用adb截图验证
android·人工智能·adb