我们知道,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.

通过直译(加揣度),大意就是包含以下两个特性:

  1. InheritedWidget是一个可以在树中高效地向下传递数据的组件:我们可以在以 InheritedWidget 为节点的树下任一Widget中调用 BuildContext.inheritFromWidgetOfExactType 来获取离其最近的 InheritedWidget 实例。
  2. 当以上面这种方式(调用inheritFromWidgetOfExactType方法时)被引用后,每当InheritedWidget自身的状态改变时,会导致 “consumer”(调用inheritFromWidgetOfExactType方法的这个Child) 重新 build

SDK提供的Theme就是基于该特性,实现了配置的传递和配置的动态改变。

案例

目前网上也有很多使用教程,核心大致实现inherited_widget_test.dart,其结构图如下所示:





我们先简单描述下代码逻辑:

  • WidgetA 调用了inheritFromWidgetOfExactType方法,获得了存放在 MyInherited 对象里的 data 数据并显示在 WidgetA 内容上,同时使得 WidgetAMyInherited 产生关联;
  • 当点击 FlatButton 触发 MyWidget 状态更新时, MyStatebuild 方法回调,重新构建 MyInherited 对象传入新值,由于data发生变化, MyInherited 的方法 updateShouldNotify 中返回了true,最终使得与MyInherited 关联的 WidgetA 触发reBuild。

当我们运行并点击FlatButton后,页面表现得确实如上述所示,WidgetA的内容由 WidgetA data = 0 变为了 WidgetA data = 1,似乎 InheritedWidget 正确的使用方式正是如此,但是log里输出的却是如下:

1
2
3
4
5
6
I/flutter: onPressed
I/flutter: MyWidget build
I/flutter: MyInherited construct
I/flutter: MyInherited updateShouldNotify result = true
I/flutter: WidgetA build
I/flutter: WidgetB build

可以看到,结合前面的代码逻辑分析,理论上只有 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
6
const 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也就不会构建,每次触发 MyWidgetState 变化时,只有InheritedWidget才会重新构建。

当我们再次点击FlatButton时,日志将如下所示:

1
2
3
4
5
I/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时容易被忽略的问题,并给出推荐方案。

感谢阅读。