Flutter BuildContext 浅析
官网上关于 BuildContext 的解释是 A handle to the location of a widget in the widget tree.
。这个解释很精准,但对于不熟悉 Flutter 代码的人来说,可能有些摸不着头脑。我尝试解释一下。
BuildContext 是什么
Widget 所处的位置
A handle to the location of a widget in the widget tree. It represents the location of a widget in the widget tree.
我们可以简单来类比一下 Android 或 JS。
- Android View 系统中使用 XML 布局文件来描述 UI,它是 View 树
- JS 对应的 UI 界面是 HTML 文件,可以理解为 DOM 树
- Flutter 系统中使用 Widget 树来描述 UI。
无论哪种 View 树还是 Widget 树还是 DOM 树,UI 开发过程很典型的一个需求就是定位到目标节点。
- JS 中使用 ID 或 CSS 选择器来定位 DOM 节点,对应的方法是
document.getElementById()
或者document.querySelector()
。 - Android 中使用 ID 来定位 View,对应的方法是
Activity.findViewById()
。 - Flutter 中不使用 ID 或选择器来定位节点,它使用
BuildContext
。Scaffold.of(context)
相当于 Flutter 的findViewById()
这里要表达的是,BuildContext
所扮演的角色跟 ID 或选择器有几分类似,这也是为什么官网说 BuildContext
表示 Widget 在 widget 树中所处的位置 “A handle to the location of a widget in the widget tree”。
Element
如果我们将 BuildContext
的类型打印出来,会发现它其实是一个 Element
。
1 | class Home extends StatelessWidget { |
以上代码输出如下:
I/flutter ( 6382): Home BuildContext: StatelessElement
这个日志显示这里的 BuildContext 实际类型是 StatelessElement
。
本文不对 BuildContext 和 Element 的关系展开讨论,有兴趣可以看看 Understanding Flutter BuildContext: A Guide for Developers,对应的中文翻译。
BuildContext 的特点
BuildContext
最重要的特点就是:每个 Widget 对象都有自己对应的 BuildContext
。
Each widget has its own BuildContext, which becomes the parent of the
widget returned by the StatelessWidget.build or State.build functionIn particular, this means that within a build method, the build context of the widget of the build method is not the same as the build context of the widgets returned by that build method.
1 | class MyWidget extends StatelessWidget { |
以这段简单的代码为例:
MyWidget
的 BuildContext 是第3行context
参数对应的那个 BuildContext 对象- 第4行的
Container
也有自己的 BuildContext,但不同于第3行的 BuildContext 对象
理解 BuildContext 的那些坑
of()
方法
这里有一个简单的例子,它很容易坑到 Flutter 开发者。我们通过这个例子来理解 BuildContext 的作用,加深对其含义及特点的理解。
- BuildContext 的含义 - 它表示 Widget 在 widget 树中所处的位置
- BuildContext 的特点 - 每个 Widget 都有自己对应的 BuildContext
示例来自 BuildContext class - widgets library - Dart API)。代码如下:
1 | class HomeWrong extends StatelessWidget { |
问题出在第10行,Scaffold.of(context)
返回 null
,所以没法弹出 BottomSheet。对应的错误日志如下:
1 | Scaffold.of() called with a context that does not contain a Scaffold. |
怪了,Widget 树中明明有 Scaffold
,为什么会找不到?
前面说过,可以将 BuildContext 所扮演的角色跟 JS 中 ID 或选择器类比,它表示当前 Widget 在 widget 树中所处的位置;另外,每个 Widget 有自己的 BuildContext,所以很容易解释为什么 Scaffold.of(context)
返回 null
。
Scaffold.of(context)
里面的context
指的是HomeWrong
对应的 BuildContext- 在 widget 树上沿着
HomeWrong
往上找,是不存在 Scaffold Widget 的
如果以 JS 或 Android 的视角来看,上面代码的问题在于用错了 ID 或选择器。
Flutter 中除了 build()
方法中可以拿到 BuildContext,还有别的办法吗?看修复后的代码:
1 | class Home extends StatelessWidget { |
注意第6行中的 Builder:
- 常规 Widget 接收
child
或children
参数,将一个或多个 Widget 作为子节点,而不对外暴露对应的 BuildContext 参数。 - Builder 是一个特殊的 Widget。它不接收
child
或children
参数,而是提供builder
回调对外暴露对应的 BuildContext 参数。
我们使用第6行的 Builder 可以获取 BuildContext,利用这个 BuildContext 可以正确地找到 Scaffold。
在修复后的代码中,
Scaffold.of(context)
里面的context
指的是Builder
对应的 BuildContext- 在 widget 树上沿着
Builder
往上找,可以找到 Scaffold Widget!
不要保存 BuildContext
- Widget 在 widget 树上的位置可能发生变化,对应地,Widget 对应的 BuildContext 也会变,所以不要缓存 BuildContext 上的方法的返回值来给跨同步方法的场景使用
The BuildContext for a particular widget can change location over time as the widget is moved around the tree. Because of this, values returned from the methods on this class should not be cached beyond the execution of a single synchronous function.
- Widget 可能从 widget 树上移除,对应地,Widget 对应的 BuildContext 不再有效,所以当跨异步方法使用 BuildContext 时一定记得要检查
context.mounted
以确认 BuildContext 仍然有效
Avoid storing instances of BuildContexts because they may become invalid if the widget they are associated with is unmounted from the widget tree. If a BuildContext is used across an asynchronous gap (i.e. after performing an asynchronous operation), consider checking mounted to determine whether the context is still valid before interacting with it: