我们知道,flutter提供了一套功能粒度划分非常细的基本组件,并推荐利用组合的方式来构建布局,这确实很便捷、高效,但是也导致了Widget层次结构嵌套非常深,即使是一个很简单的页面。而不论什么页面,控件之间往往存在某些状态依赖关系,假使让一个结构形如“A->B->C->…->Z”中的A传递数据给Z,如果利用构造函数来一层层传递,效率可想而知。好在官方提供了 InheritedWidget
。
InheritedWidget的好处不言而喻,从起官方文档的定义即可知:
Base class for widgets that efficiently propagate information down the tree.
To obtain the nearest instance of a particular type of inherited widget from a build context, use BuildContext.inheritFromWidgetOfExactType.
Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state.
通过直译(加揣度),大意就是包含以下两个特性:
- InheritedWidget是一个可以在树中高效地向下传递数据的组件:我们可以在以
InheritedWidget
为节点的树下任一Widget中调用BuildContext.inheritFromWidgetOfExactType
来获取离其最近的InheritedWidget
实例。 - 当以上面这种方式(调用
inheritFromWidgetOfExactType
方法时)被引用后,每当InheritedWidget自身的状态改变时,会导致 “consumer”(调用inheritFromWidgetOfExactType
方法的这个Child) 重新build
。
SDK提供的Theme就是基于该特性,实现了配置的传递和配置的动态改变。
案例
目前网上也有很多使用教程,核心大致实现inherited_widget_test.dart,其结构图如下所示:
我们先简单描述下代码逻辑:
WidgetA
调用了inheritFromWidgetOfExactType
方法,获得了存放在MyInherited
对象里的data
数据并显示在WidgetA
内容上,同时使得WidgetA
和MyInherited
产生关联;- 当点击
FlatButton
触发MyWidget
状态更新时,MyState
的build
方法回调,重新构建MyInherited
对象传入新值,由于data
发生变化,MyInherited
的方法updateShouldNotify
中返回了true,最终使得与MyInherited
关联的WidgetA
触发reBuild。
当我们运行并点击FlatButton
后,页面表现得确实如上述所示,WidgetA
的内容由 WidgetA data = 0
变为了 WidgetA data = 1
,似乎 InheritedWidget
正确的使用方式正是如此,但是log里输出的却是如下:
1 | I/flutter: onPressed |
可以看到,结合前面的代码逻辑分析,理论上只有 WidgetA
才会reBuild,而现在却产生了 I/flutter: WidgetB build
这条记录。这是为什么呢?
原因分析
其实可以从前面提到的特性2找到答案。其中说到:InheriteWidget 状态发生变化时会rebuild相关的child。 我们知道,flutter中Widget被标识为了@immutable
,即是不可变的,那么所谓的状态发生变化就意味着InheriteWidget重新构建,由于前面代码中在InheriteWidget构造时同时也构造的其child对象,因此当InheriteWidget重新构建时也会导致child跟着重新构建,这样也就失去了 “rebuild相关的child” 的意义,
也就是说,要想特性2生效,需要保证InheriteWidget节点下的树不会被重新构建。
推荐做法
1. 使用const Widget
将InheriteWidget
的child转化为const,这样即使在重建 InheriteWidget
时,由于其child得到的是同一个对象,也就不会导致这个子树重建,选择性reBuild也就得到了保证。但是由于const特性,相关的参数也必须是常量,因此需要重写或修改的代码量相对较多,因此更推荐下面的做法。
2. 上移Child对象到InheriteWidget的Parent Widget
我们可以参考Theme的实现方式,Theme是一个普通的Widget,其build方法如下:1
2
3
4
5
6
7
8
9
10
11/// Theme.class
@override
Widget build(BuildContext context) {
return _InheritedTheme(
theme: this,
child: IconTheme(
data: data.iconTheme,
child: child,
),
);
}
其中,_InheritedTheme
就是InheritedWidget,其child参数所指向的child对象是在Theme的构造函数中传入:1
2
3
4
5
6const Theme({
Key key,
@required this.data,
this.isMaterialAppTheme = false,
@required this.child,
})
这样就避免了当触发Theme的build方法进而导致_InheritedTheme
重建时,child也被重新实例化的情况。
对于StatefulWidget来说,InheritedWidget是在State对象的build方法中实例化的,setState触发的是State的build,而StatefulWidget本身是没有重新实例化的,也就保证了child没有被重新实例化。
现在我们在保证层级结构不变的前提下,重新对上述式例进行优化,修改后的代码位于inherited_widget_test_2.dart,其中 WidgetA和WidgetB
对象已经提前于InheritedWidget在 MyWidget
构建期间实例化,当MyWidget
执行 build
方法时触发InheritedWidget构建时再作为参数传入。这样只要 MyWidget
不重新构建,child也就不会构建,每次触发 MyWidget
的 State
变化时,只有InheritedWidget才会重新构建。
当我们再次点击FlatButton时,日志将如下所示:1
2
3
4
5I/flutter: onPressed
I/flutter: MyWidget build
I/flutter: MyInherited construct
I/flutter: MyInherited updateShouldNotify result = true
I/flutter: WidgetA build
结束
InheritedWidget 是学习flutter不得不掌握的一个特殊组件,具备向下传值和选择性reBuild子树节点的特性,许多SDK组件包括第三方库包都是基于其特性实现,本文结合官方文档发现并解决了一个使用InheritedWidget时容易被忽略的问题,并给出推荐方案。
感谢阅读。