InheritedWidget简介

Widget篇中,讲述了StatefulWidget如何管理自身的状态。但是开发一款App经常会出现多个页面数据共享的场景。于是Flutter提供了一个功能型的组件来解决数据传递的问题——InheritedWidget


数据获取

要理解InheritedWidget的如何传递数据的,要先解析其实现思路,然后带着这个思路去看源码,这样就会清晰很多。

实现思路

通常情况下,某个模块内的数据共享,全局数据共享,数据都是向下传递的。由于Flutter中整个UI架构是由Element Tree支撑的树状结构,并且只对我们暴露Widget树,那么我们可以把数据可以存放在某个Element对象持有的Widget上,并通过某个特定的方式,让子叶节点可以拿到这个Element对象,从而间接拿到Element对象上存储的数据。

Flutter是正通过如下步骤,实现上述思路的:

  1. 继承InheritedWidget并设置数据
    为了区分Element是否携带数据,Flutter定义了一个特定的Element——InheritedElement,和其持有的配置文件InheritedWidget
    当我们在构建UI树,需要在某个节点存放数据时,我们可以继承InheritedWidget,并且定义一些数据data。对于Widget层,我们暂时只关心这些就够了,其他交给内部InheritedElement去处理。

  2. 生成一个映射表吗,并向下传递
    思路中提到一个特定的方式,关于这个特定的方式,Flutter给出的方案是使用runtimeType作为key生成一张与Element的映射表,子节点根据这个key去查找对应的Element,获取数据。
    我们开发业务时,数据通常是不同的,因此在第一步中创建的子InheritedWidget的也是不同的,可以使用此WidgetruntimeType作为映射表的key。
    在每个Element生成时,会从父Element拷贝一份映射表,如果自己为数据节点InheritedElement,则把自己也添加进去。最后每个Element都会持有一份包含所有携带数据的InheritedElement的“目录”,层级越深的节点,“目录”信息也就越多。

  3. 查询映射表,获取数据
    在每个节点,我们要获取上层数据时,只需要传入数据节点的runtimeType就可以拿到数据了。

原理理解了,进入源码世界一探究竟。

源码解析

直接看下关键的ElementInheritedElement类:

/// 通用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也是这么想的。

  1. 记录依赖关系
    Widget家族中,实际掌权的是Element,每次获取数据,都是由Element对象去操办的。于是我们可以在数据节点InheritedElement中,定义一个依赖表,当每次一个Element使用到此节点时,将其加入依赖表。

  2. 子节点选择性响应
    当数据节点发生变化时,仅通知与依赖于它的节点进行响应。并且当数据未发生变化时,不通知子节点。

源码解析

思路很简单,来看下源码里是怎么实现的:
初看源码时,会注意到一个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

从运行结果和日志中,可以发现,如下几个结论:

  1. 初次加载时会出现“加工资啦” => firstBuild时,会调用didChangeDependencies方法。
  2. 后续“加班”只会减少发量不会加工资 => 只有使用context.dependOnInheritedWidgetOfExactType<DemoInheritedWidget>()获取数据间接注册依赖的State才会执行didChangeDependencies方法。
  3. 不管有没有依赖,Widgetbuild方法始终都是会执行的。难道也重构了?其实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就是对InheritedWidgetaspect使用上的一个封装。

源码解析

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实际就是对InheritedWidgetupdateShouldNotify方法的一个拓展。重写updateShouldNotifyDependent方法,根据数据与aspect的关系,通知指定类别子节点做出响应。而子节点通过InheritedModel.inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect })方法注册片面依赖关系。


总结

InheritedWidget是Flutter中非常重要的一个功能型组件,但是我们通常不会直接使用他,而是对他进行一定程度的封装后再使用。
本文简单摸索了其实现原理,后续会继续学习与InheritedWidget相关的各种有意思的封装。
以上仅是自己阅读源码时的理解,若有错误之处,欢迎大家指出,一起探讨!

【Flutter脱发实录】盘一盘InheritedWidget相关推荐

  1. python 找质数的个数_盘一盘 Python 系列特别篇 All 和 Any

    本文含 3758 字,9图表截屏建议阅读 10 分钟 本文是 Python 系列的特别篇的第十四篇 特别篇 1 - PyEcharts TreeMap 特别篇 2 - 面向对象编程 特别篇 3 - 两 ...

  2. 01-1制作U盘启动盘--大白菜超级U盘启动盘制作工具

    使用大白菜超级U盘启动盘制作工具制作U盘启动盘  工具/材料: 电脑.U盘.浏览器.大白菜u盘启动制作工具. 操作方法: 打开浏览器,输入大白菜,点击普通下载进行大白菜u盘启动制作工具下载: 或者通过 ...

  3. 用rufus f2 制作Ubuntu16.04 U盘启动盘

    用UltraISO制作Ubuntu16.04 U盘启动盘 1,从Ubuntu官网http://cn.ubuntu.com/download/下载系统的iso文件        用来制作的U盘需要是FA ...

  4. SAP 盘盈盘亏移动类型701702 Vs 711712

    SAP 盘盈盘亏移动类型701&702 Vs 711&712 SAP MM模块里有很多移动类型.与盘点相关的移动类型有701/702/703/704/707/708, 还有711/71 ...

  5. mac 环境下 制作windows系统U盘启动盘

    mac 环境下 制作windows系统U盘启动盘 下载系统文件   ylmf.iso 转换为img文件 hdiutil convert /Users/os/Downloads/ylmf.iso -fo ...

  6. 用UltraISO制作支持windows 7的U盘启动盘

    用UltraISO制作U盘启动盘,有人写过,我也看过,不过依照网上的那些文章,成功的并不多,经过几次试验,在不同的主板环境下成功概率高的方法应该如下: 1. UltraISO建议9.3以上 2. 制作 ...

  7. dell笔记本电脑驱动_戴尔Dell电脑u盘启动盘重装win10系统步骤

    戴尔Dell电脑作为一个知名的电脑品牌,有很多朋友都在使用,拥有不同类型的笔记本电脑.但是,你知道戴尔Dell电脑怎么u盘重装win10系统吗?下面就来看看大白菜整理的资料,学会戴尔Dell电脑u盘启 ...

  8. 微软制作工具_大白菜U盘启动盘制作

    系统镜像一般为ISO格式,ISO文件里面含有GHO/WIM/ESD等系统安装文件,安装系统实际上就是将GHO WIM或ESD等文件还原/解压到硬盘分区上并重建Windows系统引导的过程.一般情况下G ...

  9. 用UltraISO制作Ubuntu20.04 U盘启动盘

    用UltraISO制作Ubuntu20.04 U盘启动盘 下载ubuntu系统 首先到官网下载新版系统 https://cn.ubuntu.com/download/desktop 制作启动盘 下载U ...

最新文章

  1. 深度学习中的优化算法串讲
  2. 在Linux和Windows操作系统中socket program的兼容问题
  3. mysql并发 node_nodejs下mysql性能测试
  4. Spring整合Struts的几种最常见方式
  5. hall's marriage theorem
  6. Android原生开发modules方式导入Unity问题汇总
  7. 0301 - 一个比价的小项目
  8. 数据结构:试设计一个算法,改造一个带表头结点的双向链表,所有结点的原有次序保持在各个结点的右链域rLink中,并利用左链域ILink把所有结点按照其值从小到大的顺序连接起来
  9. noip2011day1题解
  10. 当Java 8 Streams API不够用时
  11. maven的eclipse找不到本地仓库的的jar包
  12. Linux Shell脚本入门教程系列之(十四) Shell Select教程
  13. 机器学习降维算法四:Laplacian Eigenmaps 拉普拉斯特征映射
  14. Android应用开发实例篇(1)-----简易涂鸦板
  15. vm安装diagram
  16. Angular之constructor和ngOnInit差异及适用场景
  17. 虚拟搭建局域网模拟器_雷电模拟器及夜神模拟器使用局域网连接 IDE 及抓色器...
  18. Windows安装AdelaiDet的血与泪
  19. upc 去除干员 (delete)
  20. SEO人员,为什么要做流量过滤,如何操作?

热门文章

  1. 基于ssm+vue的健身房管理系统
  2. 实训计算机硬盘分区的心得体会,计算机实训报告
  3. mysql中创建视图、索引
  4. [py]python之信用卡ATM
  5. 传输层协议 —— UDP
  6. HTTPS网页打开缓慢或者打不开
  7. 敏感词过滤 - DFA算法[确定有穷自动机]的Java 实现
  8. oracle分析函数-开窗函数
  9. 详述如何退出 Vim 编辑器
  10. 使用cmd命令行或运行框进行关机重启操作