Flutter BuildContext 浅析

官网上关于 BuildContext 的解释是 A handle to the location of a widget in the widget tree.。这个解释很精准,但对于不熟悉 Flutter 代码的人来说,可能有些摸不着头脑。我尝试解释一下。

BuildContext 是什么

Widget 所处的位置

BuildContext 官方文档中提到:

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 或选择器来定位节点,它使用 BuildContextScaffold.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
2
3
4
5
6
7
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('Home BuildContext: ${context.runtimeType}');
return Scaffold(...)
}
}

以上代码输出如下:

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 function

In 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
2
3
4
5
6
7
8
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Text('Hello, Flutter!'),
);
}
}

以这段简单的代码为例:

  • 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
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
class HomeWrong extends StatelessWidget {
@override
Widget build(BuildContext context) {
// here, Scaffold.of(context) returns null
return Scaffold(
appBar: AppBar(title: const Text('Demo')),
body: TextButton(
child: const Text('BUTTON'),
onPressed: () {
Scaffold.of(context).showBottomSheet(
(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);
},
)
],
),
),
);
},
);
},
));
}
}

问题出在第10行,Scaffold.of(context) 返回 null,所以没法弹出 BottomSheet。对应的错误日志如下:

1
2
Scaffold.of() called with a context that does not contain a Scaffold.
No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of().

怪了,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
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
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Demo')),
body: Builder(
builder: (BuildContext context) {
return TextButton(
child: const Text('BUTTON'),
onPressed: () {
Scaffold.of(context).showBottomSheet(
(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);
},
)
],
),
),
);
},
);
},
);
},
));
}
}

注意第6行中的 Builder:

  • 常规 Widget 接收 childchildren 参数,将一个或多个 Widget 作为子节点,而不对外暴露对应的 BuildContext 参数。
  • Builder 是一个特殊的 Widget。它不接收 childchildren 参数,而是提供 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:

参考