简介
对于flutter这种大量依靠组合的widget的形式来实现用户界面的框架来说,跨组件之间的数据传递其实是比较麻烦的。比如简单的widget之间,我们可以通过构造函数来传递数据,但是当组合方式过于复杂的情况,页面层级很多,一个属性就要跨越很多层的widget来做数据传递。这样就会导致很多冗余的字段,和不必要的耦合,写起来也非常的麻烦。 所以对于这种跨层的数据传递,flutter为我们提供了三种解决方式,分别是:
- InheritedWidget
- Notifcation
- eventbus
后面的文章,会逐个分析介绍每一种方式的使用和原理。本文先聊一下InheritedWidget
InheritedWidget
对于开发过flutter,写过dart代码的同学,一定都已经用过InheritedWidget了,只是可能你都不知道是在什么时候用到过
其实flutter中有很多的代码,也是通过InheritedWidget的方式来实现的。 比如说最常用的获取屏幕的宽高,获取主题等,他们都是继承自InheritedWidget的;
MediaQuery.of(context);
Theme.of(context);
简而言之,InheritedWidget 允许在 widget 树中有效地向下传播(和共享)信息。
InheritedWidget 是一个特殊的 Widget,它将作为另一个子树的父节点放置在 Widget 树中。该子树的所有 widget 都必须能够与该 InheritedWidget 暴露的数据进行交互。
使用
下面来看下 InheritedWidget如何使用,以一个小demo为例: 我们有一个用户信息的类 UserBean,当在某一个页面改了用户信息中的数据,在其他页面如何及时刷新用户信息的数据呢?
class UserBean {String name;String address;
UserBean({this.name, this.address});
@overrideString toString() { return 'UserBean{name: $name, address:$address}';}
}
首先创建一个用户信息的UserinfoInheritedWidget 继承自InheritedWidget,目的在于为子树中的所有 widget 提供用户信息数据:
class UserinfoInheritedWidget extends InheritedWidget {
UserBean userBean;
UserinfoInheritedWidget({this.userBean, Key key, Widget child}):
super(key:key, child:child);
@override
bool updateShouldNotify(UserinfoInheritedWidget oldWidget) {
// TODO: implement updateShouldNotify
if (oldWidget.userBean.name != userBean.name || oldWidget.userBean.address != userBean.address) {
return true;
}
return false;
}
}
为了能够传播/共享数据,需要将 InheritedWidget 放置在 widget 树的顶部,这解释了传递给 InheritedWidget 基础构造函数的 @required Widget child 参数。
static UserinfoInheritedWidget of(BuildContext context) 方法允许所有子 widget 通过包含的 context 获得最近的 UserinfoInheritedWidget实例(参见后面的内容)。
最后重写 updateShouldNotify 方法用来告诉 InheritedWidget 如果对数据进行了修改,是否必须将通知传递给所有子 widget(已注册/已订阅)。
然后在main.dart中 我们需要将UserinfoInheritedWidget放在树节点级别,并初始化一个用户信息UserBean:
Widget build(BuildContext context) {
return UserinfoInheritedWidget (
userBean: UserBean(name: 'flutter', address: 'China'),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
)
);
}
子节点如何访问 InheritedWidget 的数据?
然后在页面上取到用户信息,做一个简单的展示,那用户信息如何获取呢,和上面提到的获取屏幕宽高,主题是一样的,用 of函数
static UserinfoInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(UserinfoInheritedWidget);
}
在构建子节点时,后者将获得 InheritedWidget 的引用
class _MyHomePageState ...{
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
UserinfoWidget.of(context).userBean.name,
),
Text(
UserinfoWidget.of(context).userBean.address,
style: Theme.of(context).textTheme.display1,
),
],
),
),
);
}
}
如何在 Widget 之间进行交互?
InheritedWidget继承自ProxyWidget,ProxyWidget继承自Widget,可以单独使用,但是没有状态,为了有状态,一般和StatefulWidget搭配使用
class UserinfoWidget extends StatefulWidget {
UserBean userBean;
Widget child;
UserinfoWidget({this.userBean, Key key, Widget child}):
super(key:key);
static UserinfoInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(UserinfoInheritedWidget);
}
@override
_UserinfoWidgetState createState() => _UserinfoWidgetState();
}
class _UserinfoWidgetState extends State<UserinfoWidget> {
void _update(String name, String address) {
widget.userBean = UserBean(name: name, address: address);
setState(() {
});
}
@override
Widget build(BuildContext context) {
return UserinfoInheritedWidget(
updateInfo: _update,
userBean: widget.userBean,
child: widget.child,
);
}
}
继续在UserinfoInheritedWidget中添加更新的方法
class UserinfoInheritedWidget extends InheritedWidget {
UserBean userBean;
Function updateInfo;
UserinfoInheritedWidget({this.userBean, Key key, Widget child, this.updateInfo}):
super(key:key, child:child);
void updateUserBean (String name, String address) {
updateInfo(name, address);
}
@override
bool updateShouldNotify(UserinfoInheritedWidget oldWidget) {
// TODO: implement updateShouldNotify
if (oldWidget.userBean.name != userBean.name || oldWidget.userBean.address != userBean.address) {
return true;
}
return false;
}
}
至此,就是InheritedWidget的全部使用方法,上面有一个最主要的问题,就是 context.inheritFromWidgetOfExactType(UserinfoInheritedWidget) 在内部,除了简单地返回 UserinfoInheritedWidget实例外,它还订阅消费者 widget 以便用于通知更改。
在幕后,对这个静态方法的简单调用实际上做了 2 件事:
- 消费者 widget 被自动添加到订阅者列表中,从而当对 InheritedWidget应用修改时,该 widget 能够重建
- InheritedWidget中引用的数据将返回给消费者
源码分析
这个方法为什么就可以获取到UserinfoInheritedWidget的实例呢?
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
Map<Type, InheritedElement> _inheritedWidgets; _inheritedWidgets是这样定义的一个map,以type为key,所以用我们传入的UserinfoInheritedWidget这个type去找与他对应的InheritedElement 再看下这个map是如何赋值的:
void _updateInheritance() {
assert(_active);
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
}
通过分析源码就可以明白,这里就是把type作为可以 自身this作为value,存储在这个map中, 假设一个子节点InheritedWidget的实例,持有_inheritedWidgets数组,这个数组的值,首先是把这个InheritedWidget父节点的_inheritedWidgets值赋值给子节点,然后子节点在把自己的实例添加到这个数组,所以你也就可以明白为啥InheritedWidget的作用域是自己及自己的子节点了。
那_updateInheritance方法又是在何处调用呢,就是下图所示的mount方法,查看mount方法的注释可以知道在创建element的时候就会被调用
当然说到这里,还需要我们去好好了解一下,widget、element,renderobject之间的关系
阻止重新绘制
在继续访问 Inherited Widget 的同时阻止某些 Widget 重建 上面的整个介绍说明了InheritedWidget的工作原理,但是还有一个需要考虑的问题,就是,在InheritedWidget的子树中,如果某一个节点,可能只是发出一个更新的指令,但他本身并不需要重新绘制,如果我们不去处理,肯定是会降低绘制的效率的,因为每次更新都要重新绘制本不需要绘制的节点。
如前所述,调用 context.inheritFromWidgetOfExactType() 方法实际上会自动将 Widget 订阅到消费者列表中。 避免自动订阅,同时仍然允许 Widget 访问 InheritedWidget 的解决方案是通过以下方式改造 UserinfoInheritedWidget 的静态方法:
static UserinfoInheritedWidget of([BuildContext context, bool rebuild = true]) {
// return context.inheritFromWidgetOfExactType(UserinfoInheritedWidget);
return (rebuild ? context.inheritFromWidgetOfExactType(UserinfoInheritedWidget) as UserinfoInheritedWidget
: (context.ancestorWidgetOfExactType(UserinfoInheritedWidget) as UserinfoInheritedWidget).userBean);
}
通过添加一个 boolean 类型的额外参数
如果 rebuild 参数为 true(默认值),我们使用普通方法(并且将 Widget 添加到订阅者列表中) 如果 rebuild 参数为 false,我们仍然可以访问数据,但不使用 InheritedWidget 的内部实现
因此,要完成在继续访问 Inherited Widget 的同时阻止某些 Widget 重建,还需要再修改一下使用InheritedWidget的代码
UserinfoWidget.of(context, false).updateUserBean('name', 'address');
就是这样,做更新的widget,它不会再重建了。
下面附上demo代码的链接,因为代码是后面整理的,代码中最后又做了一些细节的优化,所以跟文章稍微有一些出入,但是对InheritedWidget的使用是没问题的 demo代码传送门