InheritedWidget简介
在Widget
篇中,讲述了StatefulWidget
如何管理自身的状态。但是开发一款App经常会出现多个页面数据共享的场景。于是Flutter提供了一个功能型的组件来解决数据传递的问题——InheritedWidget
。
数据获取
要理解InheritedWidget
的如何传递数据的,要先解析其实现思路,然后带着这个思路去看源码,这样就会清晰很多。
实现思路
通常情况下,某个模块内的数据共享,全局数据共享,数据都是向下传递的。由于Flutter中整个UI架构是由Element Tree
支撑的树状结构,并且只对我们暴露Widget
树,那么我们可以把数据可以存放在某个Element
对象持有的Widget
上,并通过某个特定的方式,让子叶节点可以拿到这个Element
对象,从而间接拿到Element
对象上存储的数据。
Flutter是正通过如下步骤,实现上述思路的:
-
继承InheritedWidget并设置数据
为了区分Element
是否携带数据,Flutter定义了一个特定的Element
——InheritedElement
,和其持有的配置文件InheritedWidget
。
当我们在构建UI树,需要在某个节点存放数据时,我们可以继承InheritedWidget
,并且定义一些数据data
。对于Widget
层,我们暂时只关心这些就够了,其他交给内部InheritedElement
去处理。 -
生成一个映射表吗,并向下传递
思路中提到一个特定的方式,关于这个特定的方式,Flutter给出的方案是使用runtimeType
作为key
生成一张与Element
的映射表,子节点根据这个key
去查找对应的Element
,获取数据。
我们开发业务时,数据通常是不同的,因此在第一步中创建的子InheritedWidget
的也是不同的,可以使用此Widget
的runtimeType
作为映射表的key。
在每个Element
生成时,会从父Element
拷贝一份映射表,如果自己为数据节点InheritedElement
,则把自己也添加进去。最后每个Element
都会持有一份包含所有携带数据的InheritedElement
的“目录”,层级越深的节点,“目录”信息也就越多。 -
查询映射表,获取数据
在每个节点,我们要获取上层数据时,只需要传入数据节点的runtimeType
就可以拿到数据了。
原理理解了,进入源码世界一探究竟。
源码解析
直接看下关键的Element
和InheritedElement
类:
/// 通用Element获取数据节点
abstract class Element extends DiagnosticableTree implements BuildContext {
/// 存储的映射表
Map<Type, InheritedElement> _inheritedWidgets;
/// 加载时会复制parent的映射表
void mount(Element parent, dynamic newSlot) {
_updateInheritance();
}
void _updateInheritance() {
_inheritedWidgets = _parent?._inheritedWidgets;
}
/// 根据InheritedWidget子类的泛型,查找对应的Widget
/// @override 重写的是BuildContext接口中定义的方法 可以通过上下文context调用
@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
{Object aspect}) {
/// 根据Type查找映射表
final InheritedElement ancestor =
_inheritedWidgets == null ? null : _inheritedWidgets[T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
return null;
}
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
...
return ancestor.widget;
}
}
/// 数据节点获取数据及注册映射表
class InheritedElement extends ProxyElement {
/// 获取Widget以获取Widget.data
@override
InheritedWidget get widget => super.widget as InheritedWidget;
/// 更新InheritedWidget维护的InheritedElement映射表
/// 当从父类获取到的表为null时,即自己为根节点时,创建新的表,并把自己添加进去
/// 当从父类获取到的表不为null时,从父类复制一张表,并把自己添加进去,key为widget.runtimeType
@override
void _updateInheritance() {
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;
}
}
小结Demo
写个简单的Demo,总结一下获取数据的流程:
class DemoInheritedWidget extends InheritedWidget {
/// 自定义需要传递的数据
int data;
Widget child;
DemoInheritedWidget({this.data, this.child});
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
class InheritedWidgetDemo extends StatefulWidget {
@override
_InheritedWidgetDemoState createState() => _InheritedWidgetDemoState();
}
class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('DemoInheritedWidget')),
body: Center(
/// 嵌入数据节点并初始化数据值
child: DemoInheritedWidget(
data: 23333,
child: Builder(
/// 通过context上下文指定DemoInheritedWidget类型获取Widget对象和data数据
builder: (context) => Text(
'从上层获取的数据:${context.dependOnInheritedWidgetOfExactType<DemoInheritedWidget>().data}'),
),
),
),
);
}
}
相关依赖响应
从上层数据节点中获取数据源的原理已经了解,当数据发生变化时,Flutter又是如何通知相关Element
的呢?不会是通知所有子节点吧?
实现思路
当数据发生变更时,我们只想让那些使用这些数据的Widget
响应就可以了。第一反应就是,把这些Widget
都记录下来,然后只通知这些Widget
响应不就解决了么?是的,Flutter也是这么想的。
-
记录依赖关系
Widget
家族中,实际掌权的是Element
,每次获取数据,都是由Element
对象去操办的。于是我们可以在数据节点InheritedElement
中,定义一个依赖表,当每次一个Element
使用到此节点时,将其加入依赖表。 -
子节点选择性响应
当数据节点发生变化时,仅通知与依赖于它的节点进行响应。并且当数据未发生变化时,不通知子节点。
源码解析
思路很简单,来看下源码里是怎么实现的:
初看源码时,会注意到一个didChangeDependencies
方法,因为这个方法会根据依赖关系,选择性执行。可以理解为一个响应方法。
其实细看源码会发现,实际有两个didChangeDependencies方法,作用是不同的。包括网上很多文章都忽视了这一点,将它们混为一谈了。
/// 通用Element类中的didChangeDependencies方法
abstract class Element extends DiagnosticableTree implements BuildContext {
/// 发生依赖时进行的响应 默认为标记重构方法 即调用此方法将立即标记重构,不需要进行diff操作
void didChangeDependencies() {
/// 标记此节点需要重构 交由BuildOwner处理
markNeedsBuild();
}
}
/// State中的didChangeDependencies方法
abstract class State<T extends StatefulWidget> with Diagnosticable {
/// State中自己的didChangeDependencies方法 与Element无关
/// 默认空实现,目的是用来在接收到依赖变更通知响应时,从build方法中抽离出来一部分耗资源的操作,避免build方法卡顿
@protected
void didChangeDependencies() { }
}
/// StatefulElement中关联两个didChangeDependencies方法
class StatefulElement extends ComponentElement {
/// State是否需要执行didChangeDependencies
bool _didChangeDependencies = false;
/// 持有的State对象
State<StatefulWidget> _state;
/// 重写父类Element的方法,在重构之后,标记需要State执行didChangeDependencies操作
@override
void didChangeDependencies() {
super.didChangeDependencies();
_didChangeDependencies = true;
}
/// 在重构后,调用State的didChangeDependencies方法
@override
void performRebuild() {
if (_didChangeDependencies) {
_state.didChangeDependencies();
_didChangeDependencies = false;
}
super.performRebuild();
}
}
如何做到只通知相关依赖的呢?同样用一张依赖表做记录。
/// 数据节点自己管理依赖表
class InheritedElement extends ProxyElement {
/// 依赖表 暂时只关注key 不关注value
final Map<Element, Object> _dependents = HashMap<Element, Object>();
/// 查找依赖
@protected
Object getDependencies(Element dependent) {
return _dependents[dependent];
}
/// 设置依赖
@protected
void setDependencies(Element dependent, Object value) {
_dependents[dependent] = value;
}
/// 默认设置value为null的依赖关系 可被子类重写自定义aspect
@protected
void updateDependencies(Element dependent, Object aspect) {
setDependencies(dependent, null);
}
/// 通知依赖的Element进行依赖变更时的操作
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}
/// 更新
@override
void updated(InheritedWidget oldWidget) {
/// 根据Widget中定义的条件 判断是否需要通知子节点
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget); // => super => ProxyElement => notifyClients()
}
/// 通知依赖的逻辑 即上述方法的super.updated()
@override
void notifyClients(InheritedWidget oldWidget) {
/// 遍历依赖表中的Element 依次调用didChangeDependencies()
for (final Element dependent in _dependents.keys) {
notifyDependent(oldWidget, dependent);
}
}
}
众所周知Widget
是对外暴露配置信息,因此InheritedWidget
中提供了一个抽象方法留给我们定义通知子节点响应的时机:
abstract class InheritedWidget extends ProxyWidget {
/// 需子类重写 告知Element是否需要通知子节点
@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
小结Demo
假如要实现这么一个功能,记录一位程序员的发量,并根据发量全网查找适合的产品。
class DemoInheritedWidget extends InheritedWidget {
/// 自定义需要传递的数据
String coderName;
int coderHair;
Widget child;
DemoInheritedWidget({this.coderName, this.coderHair, this.child});
/// 重写更新条件 当数据不相同时 通知重构
@override
bool updateShouldNotify(DemoInheritedWidget oldWidget) {
return coderHair != oldWidget.coderHair;
}
}
class _TestWidget1State extends State<TestWidget1> {
String name = '';
int hair = 0;
String recGoods = '';
@override
Widget build(BuildContext context) {
print('患者信息build');
DemoInheritedWidget data =
context.dependOnInheritedWidgetOfExactType<DemoInheritedWidget>();
name = data.coderName;
hair = data.coderHair;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('患者姓名:$name', style: TextStyle(fontSize: 20)),
Text('当前发量:$hair', style: TextStyle(fontSize: 20)),
Text('推荐产品:$recGoods', style: TextStyle(fontSize: 20))
],
);
}
@override
void didChangeDependencies() async {
recGoods = await Future<String>.delayed(
Duration(seconds: 3), () => '霸王防脱${(10000 - hair) ~/ 1000}号');
print('推荐产品更新');
/// 重构页面
setState(() {});
super.didChangeDependencies();
}
}
@override
void didChangeDependencies() async {
/// 模拟耗时操作 3秒出结果
recGoods = await Future<String>.delayed(
Duration(seconds: 3), () => '霸王防脱${(10000 - hair) ~/ 1000}号');
/// 重构页面
setState(() {});
super.didChangeDependencies();
}
}
class _TestWidget2State extends State<TestWidget2> {
int salary = 2000;
@override
Widget build(BuildContext context) {
print('工资build');
return Text('当前工资:$salary¥',
style: TextStyle(fontSize: 20, color: Colors.red));
}
@override
void didChangeDependencies() {
setState(() {
salary += 1000;
});
print('加工资啦');
super.didChangeDependencies();
}
}
class InheritedWidgetDemo extends StatefulWidget {
@override
_InheritedWidgetDemoState createState() => _InheritedWidgetDemoState();
}
class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> {
int num = 10000;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('DemoInheritedWidget')),
body: Center(
child: DemoInheritedWidget(
coderName: '爱新觉罗狗剩儿',
coderHair: num,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [TestWidget2(), TestWidget1()],
))),
floatingActionButton: FloatingActionButton(
child: Text('加班'),
onPressed: () {
setState(() {
num -= 1000;
});
}));
}
}
运行结果如下图:
控制台日志如下:
I/flutter ( 4515): 加工资啦
I/flutter ( 4515): 工资build
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 推荐产品更新
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 工资build
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 推荐产品更新
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 工资build
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 推荐产品更新
I/flutter ( 4515): 患者信息build
从运行结果和日志中,可以发现,如下几个结论:
- 初次加载时会出现“加工资啦” =>
firstBuild
时,会调用didChangeDependencies
方法。 - 后续“加班”只会减少发量不会加工资 => 只有使用
context.dependOnInheritedWidgetOfExactType<DemoInheritedWidget>()
获取数据间接注册依赖的State
才会执行didChangeDependencies
方法。 - 不管有没有依赖,
Widget
的build
方法始终都是会执行的。难道也重构了?其实build
方法只是执行了一段dart
代码生成了一个新的Widget
,也就是源码中的newWidget
,只有在Widget.canUpdate
返回true时,才会通知Element
更新。具体的更新逻辑要看Widget Tree
的变化,雨InheritedWidget
无瓜。
有个坑
demo中的数据都是使用基本数据类型,如果采用对象将其封装起来,那么在updateShouldNotify
方法中处理数据时,将会发现新老数据会是相同的。可能是因为引用类型变量,采用浅拷贝导致。
不发生依赖
从源码中可以看到,在使用dependOnInheritedWidgetOfExactType
方法获取数据之后,会默认将自己添加到ancestor
的_dependencies
依赖表中:
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
/// 添加进依赖表
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
那么如何做到万花丛中过,片叶不沾身,只获取数据不发生关系呢?
源码中提供了getElementForInheritedWidgetOfExactType
方法获取InheritedElement
,拿到了InheritedElement
就可以拿到InheritedWidget
和其数据啦!
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
这可以实现一些控制的操作,比如按钮之类的,只操作数据,而不发生Element
重构。
InheritedModel
之前跳过了一个内容,就是在管理依赖表_dependents
时,只用到了key
值,没有用到value
值。
final Map<Element, Object> _dependents = HashMap<Element, Object>();
void updateDependencies(Element dependent, Object aspect) {
setDependencies(dependent, null);
}
在源码中,使用aspect
命名了这个value
值,aspect
即方面、片面的意思。也就是说,当前的Element
只关注数据model
的某个方面,换个角度来说,可以将相关依赖的子节点通过aspect
进行分来,从而达成分类通知的功能。而InheritedModel
就是对InheritedWidget
在aspect
使用上的一个封装。
源码解析
InheritedModel
源码内容很少,所以直接分析源码:
abstract class InheritedModel<T> extends InheritedWidget {
/// 表示当前节点是否属于某个方面aspect 需要由子类重写 默认为true
bool isSupportedAspect(Object aspect) => true;
/// 重写此方法,根据注册时指定的aspect 定义是否需要调用子节点的didChangeDependencies方法
bool updateShouldNotifyDependent(covariant InheritedModel<T> oldWidget, Set<T> dependencies);
/// 核心方法
static T inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect }) {
/// 没有指定aspect 则找到最近的一个数据节点
if (aspect == null)
return context.dependOnInheritedWidgetOfExactType<T>();
/// 创建一个空的列表
final List<InheritedElement> models = <InheritedElement>[];
/// 向上递归查找 直到找到第一个支持aspect的节点或数据源T节点 将所有中间节点记录到列表中
_findModels<T>(context, aspect, models);
if (models.isEmpty) {
return null;
}
/// 以下代码的作用是 获取到支持aspect 的数据节点 T 并且将与T之间所有节点都使用当前aspect注册依赖关系
final InheritedElement lastModel = models.last;
for (final InheritedElement model in models) {
final T value = context.dependOnInheritedElement(model, aspect: aspect) as T;
if (model == lastModel)
return value;
}
return null;
}
/// 向上逐级递归查找符合条件InheritedElement
static void _findModels<T extends InheritedModel<Object>>(BuildContext context, Object aspect, List<InheritedElement> results) {
final InheritedElement model = context.getElementForInheritedWidgetOfExactType<T>();
/// 当前节点不是T的子节点时 跳出递归
if (model == null)
return;
results.add(model);
final T modelWidget = model.widget as T;
/// 当查找到第一个 支持aspect 的节点时 跳出递归
if (modelWidget.isSupportedAspect(aspect))
return;
Element modelParent;
model.visitAncestorElements((Element ancestor) {
modelParent = ancestor;
return false;
});
if (modelParent == null)
return;
_findModels<T>(modelParent, aspect, results);
}
}
class InheritedModelElement<T> extends InheritedElement {
/// 使用aspect注册依赖
@override
void updateDependencies(Element dependent, Object aspect) {
final Set<T> dependencies = getDependencies(dependent) as Set<T>;
if (dependencies != null && dependencies.isEmpty)
return;
if (aspect == null) {
setDependencies(dependent, HashSet<T>());
} else {
setDependencies(dependent, (dependencies ?? HashSet<T>())..add(aspect as T));
}
}
/// 根据widget定义的更新条件决定是否执行dependent.didChangeDependencies()
@override
void notifyDependent(InheritedModel<T> oldWidget, Element dependent) {
final Set<T> dependencies = getDependencies(dependent) as Set<T>;
if (dependencies == null)
return;
if (dependencies.isEmpty || widget.updateShouldNotifyDependent(oldWidget, dependencies))
dependent.didChangeDependencies();
}
}
小结
InheritedModel
实际就是对InheritedWidget
中updateShouldNotify
方法的一个拓展。重写updateShouldNotifyDependent
方法,根据数据与aspect
的关系,通知指定类别子节点做出响应。而子节点通过InheritedModel.inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect })
方法注册片面依赖关系。
总结
InheritedWidget
是Flutter中非常重要的一个功能型组件,但是我们通常不会直接使用他,而是对他进行一定程度的封装后再使用。
本文简单摸索了其实现原理,后续会继续学习与InheritedWidget
相关的各种有意思的封装。
以上仅是自己阅读源码时的理解,若有错误之处,欢迎大家指出,一起探讨!