【Flutter脱发实录】盘一盘InheritedWidget
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调用@overrideT 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;}@overrideInheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {...return ancestor.widget;}
}
/// 数据节点获取数据及注册映射表
class InheritedElement extends ProxyElement {/// 获取Widget以获取Widget.data@overrideInheritedWidget get widget => super.widget as InheritedWidget;/// 更新InheritedWidget维护的InheritedElement映射表/// 当从父类获取到的表为null时,即自己为根节点时,创建新的表,并把自己添加进去/// 当从父类获取到的表不为null时,从父类复制一张表,并把自己添加进去,key为widget.runtimeType@overridevoid _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});@overridebool updateShouldNotify(InheritedWidget oldWidget) {return true;}
}class InheritedWidgetDemo extends StatefulWidget {@override_InheritedWidgetDemoState createState() => _InheritedWidgetDemoState();
}class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> {@overrideWidget 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方法卡顿@protectedvoid didChangeDependencies() { }
}/// StatefulElement中关联两个didChangeDependencies方法
class StatefulElement extends ComponentElement {/// State是否需要执行didChangeDependenciesbool _didChangeDependencies = false;/// 持有的State对象State<StatefulWidget> _state;/// 重写父类Element的方法,在重构之后,标记需要State执行didChangeDependencies操作@overridevoid didChangeDependencies() {super.didChangeDependencies();_didChangeDependencies = true;}/// 在重构后,调用State的didChangeDependencies方法@overridevoid performRebuild() {if (_didChangeDependencies) {_state.didChangeDependencies();_didChangeDependencies = false;}super.performRebuild();}
}
如何做到只通知相关依赖的呢?同样用一张依赖表做记录。
/// 数据节点自己管理依赖表
class InheritedElement extends ProxyElement {/// 依赖表 暂时只关注key 不关注valuefinal Map<Element, Object> _dependents = HashMap<Element, Object>();/// 查找依赖@protectedObject getDependencies(Element dependent) {return _dependents[dependent];}/// 设置依赖@protectedvoid setDependencies(Element dependent, Object value) {_dependents[dependent] = value;}/// 默认设置value为null的依赖关系 可被子类重写自定义aspect@protectedvoid updateDependencies(Element dependent, Object aspect) {setDependencies(dependent, null);}/// 通知依赖的Element进行依赖变更时的操作@protectedvoid notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {dependent.didChangeDependencies();}/// 更新@overridevoid updated(InheritedWidget oldWidget) {/// 根据Widget中定义的条件 判断是否需要通知子节点if (widget.updateShouldNotify(oldWidget))super.updated(oldWidget); // => super => ProxyElement => notifyClients()}/// 通知依赖的逻辑 即上述方法的super.updated()@overridevoid notifyClients(InheritedWidget oldWidget) {/// 遍历依赖表中的Element 依次调用didChangeDependencies()for (final Element dependent in _dependents.keys) {notifyDependent(oldWidget, dependent);}}
}
众所周知Widget
是对外暴露配置信息,因此InheritedWidget
中提供了一个抽象方法留给我们定义通知子节点响应的时机:
abstract class InheritedWidget extends ProxyWidget {/// 需子类重写 告知Element是否需要通知子节点@protectedbool updateShouldNotify(covariant InheritedWidget oldWidget);
}
小结Demo
假如要实现这么一个功能,记录一位程序员的发量,并根据发量全网查找适合的产品。
class DemoInheritedWidget extends InheritedWidget {/// 自定义需要传递的数据String coderName;int coderHair;Widget child;DemoInheritedWidget({this.coderName, this.coderHair, this.child});/// 重写更新条件 当数据不相同时 通知重构@overridebool updateShouldNotify(DemoInheritedWidget oldWidget) {return coderHair != oldWidget.coderHair;}
}class _TestWidget1State extends State<TestWidget1> {String name = '';int hair = 0;String recGoods = '';@overrideWidget 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))],);}@overridevoid didChangeDependencies() async {recGoods = await Future<String>.delayed(Duration(seconds: 3), () => '霸王防脱${(10000 - hair) ~/ 1000}号');print('推荐产品更新');/// 重构页面setState(() {});super.didChangeDependencies();}
}@overridevoid 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;@overrideWidget build(BuildContext context) {print('工资build');return Text('当前工资:$salary¥',style: TextStyle(fontSize: 20, color: Colors.red));}@overridevoid didChangeDependencies() {setState(() {salary += 1000;});print('加工资啦');super.didChangeDependencies();}
}class InheritedWidgetDemo extends StatefulWidget {@override_InheritedWidgetDemoState createState() => _InheritedWidgetDemoState();
}class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> {int num = 10000;@overrideWidget 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): 患者信息buildI/flutter ( 4515): 工资build
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 推荐产品更新
I/flutter ( 4515): 患者信息buildI/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 需要由子类重写 默认为truebool 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;}/// 向上逐级递归查找符合条件InheritedElementstatic 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注册依赖@overridevoid 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()@overridevoid 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
相关的各种有意思的封装。
以上仅是自己阅读源码时的理解,若有错误之处,欢迎大家指出,一起探讨!
【Flutter脱发实录】盘一盘InheritedWidget相关推荐
- python 找质数的个数_盘一盘 Python 系列特别篇 All 和 Any
本文含 3758 字,9图表截屏建议阅读 10 分钟 本文是 Python 系列的特别篇的第十四篇 特别篇 1 - PyEcharts TreeMap 特别篇 2 - 面向对象编程 特别篇 3 - 两 ...
- 01-1制作U盘启动盘--大白菜超级U盘启动盘制作工具
使用大白菜超级U盘启动盘制作工具制作U盘启动盘 工具/材料: 电脑.U盘.浏览器.大白菜u盘启动制作工具. 操作方法: 打开浏览器,输入大白菜,点击普通下载进行大白菜u盘启动制作工具下载: 或者通过 ...
- 用rufus f2 制作Ubuntu16.04 U盘启动盘
用UltraISO制作Ubuntu16.04 U盘启动盘 1,从Ubuntu官网http://cn.ubuntu.com/download/下载系统的iso文件 用来制作的U盘需要是FA ...
- SAP 盘盈盘亏移动类型701702 Vs 711712
SAP 盘盈盘亏移动类型701&702 Vs 711&712 SAP MM模块里有很多移动类型.与盘点相关的移动类型有701/702/703/704/707/708, 还有711/71 ...
- mac 环境下 制作windows系统U盘启动盘
mac 环境下 制作windows系统U盘启动盘 下载系统文件 ylmf.iso 转换为img文件 hdiutil convert /Users/os/Downloads/ylmf.iso -fo ...
- 用UltraISO制作支持windows 7的U盘启动盘
用UltraISO制作U盘启动盘,有人写过,我也看过,不过依照网上的那些文章,成功的并不多,经过几次试验,在不同的主板环境下成功概率高的方法应该如下: 1. UltraISO建议9.3以上 2. 制作 ...
- dell笔记本电脑驱动_戴尔Dell电脑u盘启动盘重装win10系统步骤
戴尔Dell电脑作为一个知名的电脑品牌,有很多朋友都在使用,拥有不同类型的笔记本电脑.但是,你知道戴尔Dell电脑怎么u盘重装win10系统吗?下面就来看看大白菜整理的资料,学会戴尔Dell电脑u盘启 ...
- 微软制作工具_大白菜U盘启动盘制作
系统镜像一般为ISO格式,ISO文件里面含有GHO/WIM/ESD等系统安装文件,安装系统实际上就是将GHO WIM或ESD等文件还原/解压到硬盘分区上并重建Windows系统引导的过程.一般情况下G ...
- 用UltraISO制作Ubuntu20.04 U盘启动盘
用UltraISO制作Ubuntu20.04 U盘启动盘 下载ubuntu系统 首先到官网下载新版系统 https://cn.ubuntu.com/download/desktop 制作启动盘 下载U ...
最新文章
- 深度学习中的优化算法串讲
- 在Linux和Windows操作系统中socket program的兼容问题
- mysql并发 node_nodejs下mysql性能测试
- Spring整合Struts的几种最常见方式
- hall's marriage theorem
- Android原生开发modules方式导入Unity问题汇总
- 0301 - 一个比价的小项目
- 数据结构:试设计一个算法,改造一个带表头结点的双向链表,所有结点的原有次序保持在各个结点的右链域rLink中,并利用左链域ILink把所有结点按照其值从小到大的顺序连接起来
- noip2011day1题解
- 当Java 8 Streams API不够用时
- maven的eclipse找不到本地仓库的的jar包
- Linux Shell脚本入门教程系列之(十四) Shell Select教程
- 机器学习降维算法四:Laplacian Eigenmaps 拉普拉斯特征映射
- Android应用开发实例篇(1)-----简易涂鸦板
- vm安装diagram
- Angular之constructor和ngOnInit差异及适用场景
- 虚拟搭建局域网模拟器_雷电模拟器及夜神模拟器使用局域网连接 IDE 及抓色器...
- Windows安装AdelaiDet的血与泪
- upc 去除干员 (delete)
- SEO人员,为什么要做流量过滤,如何操作?