Skip to content
Linbudu's Blog

【outdated】Flutter状态管理初接触(二):InheritedWidget与React中的context

1 min read

前言

[Flutter 状态管理初接触]上一篇 中我们介绍了基于provider的状态管理, 也说到它的底层是基于InheritedWidget的. 我在上手尝试过使用后, 个人感觉很像 React 中的 context, 因此觉得在讲解InheritedWidget的过程中, 可以多以 React context 的使用来作为示例.

还看了下闲鱼技术部的 fish-redux, 感觉更熟悉了... action / reducer / store / effect 的概念简直梦回 redux/dva, 果然状态管理是殊途同归的?

我们先通过回顾下React Context API:

1const MyContext = React.createContext(defaultValue);
2const { Provider, Consumer } = MyContext;
3
4<Provider value={SHARED_CONTEXT}></Provider>
5<Consumer>{/* 跨层级消费共享上下文 */}</Consumer>

使用Provider提供状态后, 你就可以跨越任意层级的组件访问消费该状态而无需一级级的传递回调函数与属性了, 说来有趣, 我个人在初学 react 阶段, 看到 context 时简直惊为天人...

InheritedWidget的使用同样是类似的的, 将状态提升到顶层(我觉得这是比较好的写法, 不要只将其提升到有消费需求的 widget 最近公共父节点这种), 而后内部子 widget 就可以进行使用.

仍然是以计数器为例子, 我们直接来从代码领略其作用(因为它的确属于看了就会的 233):

1class Counter {
2 final int count;
3 const Counter(this.count);
4}
5
6class SharedContext extends InheritedWidget {
7 final Counter counter;
8
9 final void Function() increment;
10 final void Function() decrement;
11
12 SharedContext(
13 {Key key,
14 @required this.counter,
15 @required this.increment,
16 @required this.decrement,
17 @required Widget child})
18 : super(key: key, child: child);
19
20}

SharedContext即是我们要进行共享的上下文, 而其内部的"状态"包括counter与两个对 counter 修改的方法. 在构造函数中我们要求传入counter & increment & decrement , 是为了在InheritedWidget(可以理解为Provider)内部去实例化实际上要共享的状态, 这么做的好处是你可以将SharedContext视作一个抽象类一般的存在, 实际使用可以基于其共享多种状态(比如 widget 需要的 counter 有的从 0 开始, 有的从 100 开始).

我们还需要为SharedContext提供一个获取上下文的静态方法, 后代有消费需求的 widget 需要先以SharedContext.xxx的形式获取到共享上下文, 才能进行消费:

1static SharedContext of(BuildContext context) {
2 // 该方法用于找到最近的上下文
3 return context.dependOnInheritedWidgetOfExactType<SharedContext>();
4 }

值得一提的是, context.inheritFromWidgetOfExactType(targetType)这一方法已经被废弃, 请使用dependOnInheritedWidgetOfExactType<targetType>方法

dependOnInheritedWidgetOfExactType<SharedContext>能够找到并返回最近的指定类型的共享上下文.

接着, 我们可以来创建Provider功能的 widget 了, 它需要是一个StatefulWidget:

1import "package:flutter/material.dart";
2
3void main() {
4 runApp(InheritedWidgetContainer());
5}
6
7class InheritedWidgetContainer extends StatefulWidget {
8 InheritedWidgetContainer({Key key}) : super(key: key);
9
10 @override
11 _InheritedWidgetContainerState createState() =>
12 _InheritedWidgetContainerState();
13}
14
15class _InheritedWidgetContainerState extends State<InheritedWidgetContainer> {
16 Counter counter;
17
18 void _initialization() {
19 counter = Counter(0);
20 }
21
22 @override
23 void initState() {
24 _initialization();
25 super.initState();
26 }
27
28 void _increment() {
29 setState(() {
30 counter = Counter(counter.count + 1);
31 });
32 }
33
34 void _decrement() {
35 setState(() {
36 counter = Counter(counter.count - 1);
37 });
38 }
39
40 @override
41 Widget build(BuildContext context) {
42 return SharedContext(
43 counter: counter,
44 increment: _increment,
45 decrement: _decrement,
46 child: MaterialApp(
47 home: Scaffold(
48 appBar: AppBar(
49 title: Text("基于 InheritedWidget 的 Flutter状态管理"),
50 ),
51 body: Center(
52 child: Center(
53 child: Column(
54 mainAxisAlignment: MainAxisAlignment.center,
55 children: <Widget>[
56 // ...
57 ],
58 ),
59 )),
60 ),
61 ),
62 );
63 }
64}

InheritedWidgetContainer中, 我们提供了实例化的counter以及修改方法的实现, 并传入到SharedContext中(注意其形式并不是 redux 中的value={store}这样.) 在不同的 Container 中, 我们可以实例化不同的状态.

接着, 我们先创建一个 widget 用于读取值:

1class ValueWidget extends StatefulWidget {
2 ValueWidget({Key key}) : super(key: key);
3
4 @override
5 _ValueWidgetState createState() => _ValueWidgetState();
6}
7
8class _ValueWidgetState extends State<ValueWidget> {
9
10 @override
11 void didChangeDependencies() {
12 super.didChangeDependencies();
13 print("Dependencies change");
14 }
15
16 @override
17 Widget build(BuildContext context) {
18 final inheritedCtx = SharedContext.of(context);
19 final counter = inheritedCtx.counter;
20
21 print("Count Value in ValueWidget: ${counter.count}");
22
23 return Padding(
24 padding: EdgeInsets.all(15),
25 child: Text(
26 "count: ${counter.count}",
27 style: Theme.of(context).textTheme.headline3,
28 ),
29 );
30 }
31}

需要注意的是这里:

1final inheritedCtx = SharedContext.of(context);
2 final counter = inheritedCtx.counter;

以及didChangeDeps方法, 了解过 Flutter 生命周期的话你应该知道它会在依赖变化时调用, 用于执行一些开销比较大的操作如网络请求这种副作用, 但依赖是什么呢? 实际上就是其在 widget 树的上方所拥有的InheritedWidget父级内部状态是否变化, 我们在SharedContext中新增一个方法:

1@override
2 bool updateShouldNotify(SharedContext oldContext) {
3 return true;
4 }

这个方法接收变化前的状态, 并且判断是否触发子组件的didChangeDeps事件(根据返回 true/false). 父或祖先 widget 中的InheritedWidget改变(updateShouldNotify返回true)时会被调用. 如果 build 中没有依赖 InheritedWidget,则此回调不会被调用.

如果你在这个控件内没有使用到InheritedWidget传递的值, 那么这个方法就不会在状态变化时被调用.

接着, 我们把+ & -按钮也抽离成组件, 来展示其"共享"能力:

1class IncreWidget extends StatelessWidget {
2 const IncreWidget({Key key}) : super(key: key);
3
4 @override
5 Widget build(BuildContext context) {
6 final inheritedCtx = SharedContext.of(context);
7 final counter = inheritedCtx.counter;
8
9 print("Count Value in IncreWidget: ${counter.count}");
10
11 return Padding(
12 padding: EdgeInsets.only(bottom: 15),
13 child: RaisedButton(
14 color: Colors.white,
15 onPressed: inheritedCtx.increment,
16 child: Text(
17 "+",
18 style: TextStyle(fontSize: 32, color: Colors.green),
19 ),
20 ));
21 }
22}
23
24class DecreWidget extends StatelessWidget {
25 const DecreWidget({Key key}) : super(key: key);
26
27 @override
28 Widget build(BuildContext context) {
29 final inheritedCtx = SharedContext.of(context);
30 final counter = inheritedCtx.counter;
31
32 print("Count Value in DecreWidget: ${counter.count}");
33
34 return Padding(
35 padding: EdgeInsets.only(top: 15),
36 child: RaisedButton(
37 color: Colors.white,
38 onPressed: inheritedCtx.decrement,
39 child: Text(
40 "-",
41 style: TextStyle(fontSize: 32, color: Colors.redAccent),
42 ),
43 ));
44 }
45}

添加到Container中:

1@override
2 Widget build(BuildContext context) {
3 return SharedContext(
4 counter: counter,
5 increment: _increment,
6 decrement: _decrement,
7 child: MaterialApp(
8 home: Scaffold(
9 appBar: AppBar(
10 title: Text("基于 InheritedWidget 的 Flutter状态管理"),
11 ),
12 body: Center(
13 child: Center(
14 child: Column(
15 mainAxisAlignment: MainAxisAlignment.center,
16 children: <Widget>[
17 const IncreWidget(),
18 ValueWidget(),
19 const DecreWidget(),
20 ],
21 ),
22 )),
23 ),
24 ),
25 );
26 }

完整代码见 GitHub

其他注意点:

  • SharedContext 会在内部值更新时重新构建, 并且是懒初始化的(我猜provider的懒初始化也是因为这个)
  • SharedContext 会跟随指定类型的父级InheritedWidget, 来实现局部widget更新