跟🤡杰哥一起学Flutter (十八、进阶:🔍探探 BuildContext)

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

1. 引言

😄 上节《十七、实战进阶-用 ViewModel 来分离 UI & 逻辑》中提到在逻辑层 弹窗页面跳转 ,拿不到当前的 BuildContext , 如果在 异步操作 中传递,会显示 "Don't use 'BuildContext's across async gaps. " 的 警告

上述例子,如果在5s内,用户跳转到别的页面,可能会导致原先的 BuildContext 所对应的Widget不在Widget树中,此时尝试使用这个 BuildContext 将会引发运行时错误。建议先调用下 BuildContext 对象的 mounted 属性,为 true 才使用 BuildContext 进行操作。因为,当 WidgetWidget树 移除时,mounted 会变为 false 😁。 一种常见的解法:

定义一个 GlobalKey 类型的 顶层变量 ,在创建 MaterialApp 时,通过 navigatorKey 属性传入,然后就可以在应用的 任何地方 使用 navigatorKey.currentContext 来获取 BuildContext 。然后需要注意下,它可能会返回 null 值,你能确保它不会空的话就用 ! ,否则还是老老实实 判空 🤷‍♀️。

然后,就可以定义 全局 的 showSnackBar() 和 pop() 方便代码复用了:

🤡 Demo 可以,CV到公司项目上,一调 pop() 就 黑屏 ,改回Button里直接 Navigator.pop(context) 又正常。

感觉是 BuildContext 不对,分别 断点 了一下 Demo 和 公司项目中 BuildContext 的值进行对比:

好像看不出个所以然来...

直觉告诉我很大概率是因为搞 混合开发 集成了 flutter_boost,它接管 路由 管理导致的,翻了下文档,看到关闭页面的API → BoostNavigator.instance.pop() ,尝试把pop() 部分的代码改成这句,然后就不会黑屏了...

🤔 问题是解决了,但引起问题的具体原因却还没定位到,直接去扒 flutter_boost 有点自不量力了🤷‍♀️,毕竟连 Flutter 本身那套 路由 的机制都还没摸透,还是先搞点基础。本节来探探 BuildContext,它在Flutter开发中扮演着极其重要的角色,几乎贯穿整个 Flutter应用的开发周期,应该是本系列最轻松的一节了🤣~

2. BuildContext 简介

🔍 直接点开 BuildContext 的源码,一段注释映入眼帘 👀:

/// [BuildContext] objects are actually [Element] objects. The [BuildContext]

/// interface is used to discourage direct manipulation of [Element] objects.

简单翻译

BuildContext 实际上就是 Element 对象,定义 BuildContext 接口是为了防止开发者直接操作 Element

点开 Element 类,可以看到它实现了 BuildContext 接口:

dart 复制代码
abstract class Element extends DiagnosticableTree implements BuildContext 

往上看 BuildContext 的其它注释,还能了解到这些信息:

  • BuildContext → 一个指向 Widget树Widget位置引用 ,提供一系列 访问和操作当前Widget相关环境和数据 的方法,可以在 StatelessWidget#build()State 对象的方法中使用。
  • 有些 静态方法 也需要用到 BuildContext,如:showDialog() → 通过上下文确定在哪个Widget树中弹出对话框,Theme.of() → 通过上下文查找最近的 Theme Widget 来获得当前上下文中的主题信息。
  • 调用 Widget#build() 时传递的 BuildContext参数 代表当前正在构建的 WidgetWidget树 中的位置。当构建返回的 Widget 被插入到 Widget树 时,它就有了 自己的BuildContext ,和上面build()传入的上下文不同,因为它代表返回的Widget在树中的 新位置
  • 写了一个 ScaffoldState.showBottomSheet() 通过 Builder 组件的 builder() 获取 BuildContext 来查找到正确的 Scaffold 代码示例:
dart 复制代码
 @override
 Widget build(BuildContext context) {
   // 在这里这行 Scaffold.of(context) 会返回 null
   return Scaffold(
     appBar: AppBar(title: const Text('Demo')),
     // Builder 会提供一个新的 BuildContext,即当前Widget在Widget树中的位置
     body: Builder(
       builder: (BuildContext context) {
         return TextButton(
           child: const Text('BUTTON'),
           onPressed: () {
             Scaffold.of(context).showBottomSheet<void>(
               (BuildContext context) {
                 return Container(
                   alignment: Alignment.center,
                   height: 200,
                   color: Colors.amber,
                   child: Center(
                     child: Column(
                       mainAxisSize: MainAxisSize.min,
                       children: <Widget>[
                         const Text('BottomSheet'),
                         ElevatedButton(
                           child: const Text('Close BottomSheet'),
                           onPressed: () {
                             Navigator.pop(context);
                           },
                         )
                       ],
                     ),
                   ),
                 );
               },
             );
           },
         );
       },
     )
   );
 }

然后是 常用的属性与方法

  • widget:返回与当前BuildContext关联的Widget实例。
  • size:返回与当前BuildContext关联的Widget实例大小,此属性在 build() 中不可用,因为构建过程中 Widget 的大小还未确定。
  • mounted:与当前BuildContext关联的Widget实例是否挂载在Widget树中。
  • findAncestorWidgetOfExactType :查找最近的 祖先Widget,Widget类型是T。
  • findAncestorStateOfType :查找最近的 祖先State,State类型是T。
  • findRootAncestorStateOfType :查找 最顶层的祖先State,State类型是T。
  • findRenderObject() :返回与当前Widget相关联的 RenderObject
  • visitChildElements() :遍历当前Element的所有子Element。
  • visitAncestorElements() :遍历当前Element的所有祖先Element。
  • dependOnInheritedWidgetOfExactType ():查找最近的父级InheritedWidget,并注册依赖关系,Widget类型是T。
  • getElementForInheritedWidgetOfExactType ():查找最近的父级InheritedWidget,不注册依赖关系,Widget类型是T

最后再提一点,在《郭佬GSY博客》看到的 BuildContext使用小技巧

在异步操作里使用 of(context) 可以先 提前获取 再做 异步操作 ,这样可以尽量保证流程完整执行。另外,建议把 of(context) 相关的操作逻辑放到 didChangeDependencies() 中处理。

3. BuildContext 使用场景

😄 还挺多,不过在知道下面的原理后,就很好理解了,简单过一下吧~

BuildContextElementWidget在Widget树上的位置引用

3.1. 导航

使用 Navigator 进行页面跳转时,需要 BuildContext 来获取当前的导航状态。

dart 复制代码
Navigator.of(context).push(MaterialPageRoute(builder: (context) => NewPage()));
Navigator.of(context).pop()

调用 Navigator.of(context) 实际上是请求当前 BuildContext 所在位置向上查找最新的 NavigatorState

它提供了操作路由堆栈的相关方法,如:push()、pop()、pushNamed()、pushReplacement() 等。

WidgetsAppMaterialAppCupertinoApp 内部会自动创建一个 顶层的Navigator 来管理应用的顶层路由堆栈。定义 GlobalKey 传递分配给它们的 navigatorKey 属性,就是让其它创建的Navigator Widget使用这个 GlobalKey,从而实现全局访问。

3.2. 主题

访问当前主题信息,如颜色、字体等,需要使用 BuildContext 来获取 最近的Theme

dart 复制代码
ThemeData theme = Theme.of(context);
Color primaryColor = Theme.of(context).primaryColor;
Color accentColor = Theme.of(context).colorScheme.secondary;

3.3. 媒体查询

获取设备的屏幕尺寸、方向、像素密度等信息。

dart 复制代码
// 获得屏幕尺寸
Size screenSize = MediaQuery.of(context).size;
double screenWidth = screenSize.width;
double screenHeight = screenSize.height;

// 获取屏幕方向
Orientation orientation = MediaQuery.of(context).orientation;

// 获取设备像素密度
double devicePixelRatio = MediaQuery.of(context).devicePixelRatio;

// 获取顶部安全区域的高度:对于有刘海屏或圆角的设备,顶部安全区域指的是不被刘海遮挡且可用于显示内容的区域的高度。
double topPadding = MediaQuery.of(context).padding.top;

// 获得底部安全区域的高度:对于一些设备,底部可能有虚拟按键或者圆角,底部安全区域指的是不被这些元素遮挡且可用于显示内容的区域的高度。
double bottomPadding = MediaQuery.of(context).padding.bottom;

:需要确保MediaQuery.of()用到的context是在 MaterialApp/WidgetsApp/CupertinoApp 构建的Widget树中,因为它依赖到这些顶层Widget提供的 MediaQueryData ,找不到会抛 NoSuchMethodError

3.4. 局部化和国际化

获取当前的区域设置信息,用于国际化。

dart 复制代码
// 获取当前Locale
Locale locale = Localizations.localeOf(context);

// 使用获取的Locale信息
Text('Current Locale: ${myLocale.languageCode}-${myLocale.countryCode}')

3.5. 弹窗和对话框

显示弹窗、底部表单等需要 BuildContext 来标识从哪个部分的界面弹出。

dart 复制代码
// 弹窗
showDialog(
  context: context,
  builder: (BuildContext context) {
    return AlertDialog(
      title: Text("Title"),
      content: Text("Content"),
    );
  },
);

// 弹出底部表单
showBottomSheetExample(BuildContext context) {
  showModalBottomSheet(
    context: context,
    builder: (BuildContext context) {
      return Container(
        height: 200,
        color: Colors.amber,
        child: Center(
          child: Text('这是一个底部表单'),
        ),
      );
    },
  );
}

3.6. 状态管理

在使用 Provider、InheritedWidget 等状态管理工具时,BuildContext 用于获取最近的状态或数据。

dart 复制代码
// Provider
final myModel = Provider.of(context);

// InheritedWidget
InheritedMyModel data = context.dependOnInheritedWidgetOfExactType();

3.7. 访问 Scaffold

如,显示一个 SnackBar 需要 BuildContext 来查找最近的 Scaffold

dart 复制代码
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Hello")));

3.8. 获取 Form 状态

在使用 Form Widget 时,BuildContext 用于获取 FormState,进而对表单进行操作,如验证表单。

dart 复制代码
FormState formState = Form.of(context);

3.9. 资源读取

如读取图片、加载文本文件等,可以通过 BuildContext 来获取当前的 AssetBundle。

dart 复制代码
DefaultAssetBundle.of(context).loadString('assets/config.json');
相关推荐
桃花仙丶3 分钟前
iOS/Flutter混合开发之PlatformView配置与使用
flutter·ios·xcode·swift·dart
Bruce_Liuxiaowei7 分钟前
HarmonyOS Next~鸿蒙系统UI创新实践:原生精致理念下的设计革命
ui·华为·harmonyos
拉不动的猪38 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程1 小时前
ES练习册
java·前端·elasticsearch
Asthenia04121 小时前
Netty编解码器详解与实战
前端
袁煦丞1 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员3 小时前
layui时间范围
前端·javascript·layui
NoneCoder3 小时前
HTML响应式网页设计与跨平台适配
前端·html