一,概述  

  业务开发中经常会碰到这样的情况,多个Widget需要同步同一份全局数据,比如点赞数、评论数、夜间模式等等。在安卓中,一般的实现方式是观察者模式,需要开发者自行实现并维护观察者的列表。在flutter中,原生提供了用于Widget间共享数据的InheritedWidget,当InheritedWidget发生变化时,它的子树中所有依赖了它的数据的Widget都会进行rebuild,这使得开发者省去了维护数据同步逻辑的麻烦。
  InheritedWidget是一个特殊的Widget,开发者可以将其作为另一个子树的父级放在Widgets树中。该子树的所有小部件都必须能够与该InheritedWidget公开的数据进行交互。

二,源码分析

  • InheritedWidget
    先来看下InheritedWidget的源码:

    1. abstract class InheritedWidget extends ProxyWidget {
    2. const InheritedWidget({ Key key, Widget child })
    3. : super(key: key, child: child);
    4.  
    5. @override
    6. InheritedElement createElement() => new InheritedElement(this);
    7.  
    8. @protected
    9. bool updateShouldNotify(covariant InheritedWidget oldWidget);
    10. }

    它继承自ProxyWidget:

    1. abstract class ProxyWidget extends Widget {
    2. const ProxyWidget({ Key key, @required this.child }) : super(key: key);
    3. final Widget child;
    4. }

    可以看出Widget内除了实现了createElement方法外没有其他操作了,它的实现关键一定就是InheritedElement了。

  • InheritedElement
    来看下InheritedElement源码

    1. class InheritedElement extends ProxyElement {
    2. InheritedElement(InheritedWidget widget) : super(widget);
    3.  
    4. @override
    5. InheritedWidget get widget => super.widget;
    6.  
    7. // 这个Set记录了所有依赖的Element
    8. final Set<Element> _dependents = new HashSet<Element>();
    9.  
    10. // 该方法会在Element mount和activate方法中调用,_inheritedWidgets为基类Element中的成员,用于提高Widget查找父节点中的InheritedWidget的效率,它使用HashMap缓存了该节点的父节点中所有相关的InheritedElement,因此查找的时间复杂度为o(1)
    11. @override
    12. void _updateInheritance() {
    13. final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    14. if (incomingWidgets != null)
    15. _inheritedWidgets = new HashMap<Type, InheritedElement>.from(incomingWidgets);
    16. else
    17. _inheritedWidgets = new HashMap<Type, InheritedElement>();
    18. _inheritedWidgets[widget.runtimeType] = this;
    19. }
    20.  
    21. // 该方法在父类ProxyElement中调用,看名字就知道是通知依赖方该进行更新了,这里首先会调用重写的updateShouldNotify方法是否需要进行更新,然后遍历_dependents列表并调用didChangeDependencies方法,该方法内会调用mardNeedsBuild,于是在下一帧绘制流程中,对应的Widget就会进行rebuild,界面也就进行了更新
    22. @override
    23. void notifyClients(InheritedWidget oldWidget) {
    24. if (!widget.updateShouldNotify(oldWidget))
    25. return;
    26. for (Element dependent in _dependents) {
    27. dependent.didChangeDependencies();
    28. }
    29. }
    30. }

    其中_updateInheritance方法在基类Element中的实现如下:

    1. void _updateInheritance() {
    2. _inheritedWidgets = _parent?._inheritedWidgets;
    3. }

    总结来说就是Element在mount的过程中,如果不是InheritedElement,就简单的将缓存指向父节点的缓存,如果是InheritedElement,就创建一个缓存的副本,然后将自身添加到该副本中,这样做会有两个值得注意的点:

    1. InheritedElement的父节点们是无法查找到自己的,即InheritedWidget的数据只能由父节点向子节点传递,反之不能。
    2. 如果某节点的父节点有不止一个同一类型的InheritedWidget,调用inheritFromWidgetOfExactType获取到的是离自身最近的该类型的InheritedWidget。
  • 看到这里似乎还有一个问题没有解决,依赖它的Widget是在何时被添加到_dependents这个列表中的呢?

    回忆一下从InheritedWidget中取数据的过程,对于InheritedWidget有一个通用的约定就是添加static的of方法,该方法中通过inheritFromWidgetOfExactType找到parent中对应类型的的InheritedWidget的实例并返回,与此同时,也将自己注册到了依赖列表中,该方法的实现位于Element类,实现如下:

    1. @override
    2. InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
    3. // 这里通过上述mount过程中建立的HashMap缓存找到对应类型的InheritedElement
    4. final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    5. if (ancestor != null) {
    6. // 这个列表记录了当前Element依赖的所有InheritedElement,用于在当前Element deactivate时,将自己从InheritedElement的_dependents列表中移除,避免不必要的更新操作
    7. _dependencies ??= new HashSet<InheritedElement>();
    8. _dependencies.add(ancestor);
    9. // 这里将自己添加到了_dependents列表中,相当于注册了监听
    10. ancestor._dependents.add(this);
    11. return ancestor.widget;
    12. }
    13. _hadUnsatisfiedDependencies = true;
    14. return null;
    15. }

三,示例demo

  • 自定义继承于InheritedWidget的类

    1. //模型数据
    2. class InheritedTestModel {
    3. final int count;
    4. const InheritedTestModel(this.count);
    5. }
    6.  
    7. //自定义MyInheritedWidget(可以把它看成古代边城的哨所)
    8. class MyInheritedWidget extends InheritedWidget {
    9. //构造方法
    10. MyInheritedWidget({
    11. Key key,
    12. @required this.data,
    13. @required Widget child //子组件
    14. }):super(key:key,child:child);
    15.  
    16. //属性
    17. final InheritedTestModel data;//共享的数据model
    18.  
    19. //类方法
    20. static MyInheritedWidget of(BuildContext context){
    21. return context.inheritFromWidgetOfExactType(MyInheritedWidget);
    22. }
    23.  
    24. //示例方法
    25. @override
    26. bool updateShouldNotify(MyInheritedWidget oldWidget) {
    27. // TODO: implement updateShouldNotify
    28. return data != oldWidget.data;
    29. }
    30. }

    代码解析

    • 此代码定义了一个名为“MyInheritedWidget”的Widget,旨在“共享”所有小部件(与子树的一部分)中的某些数据(data)。

      如前所述,为了能够传播/共享一些数据,需要将InheritedWidget定位在窗口小部件树的顶部,这解释了传递给InheritedWidget基础构造函数的“@required Widget child”。

    • Static MyInheritedWidget(BuildContext context)”方法允许所有子窗口小部件获取最接近上下文的MyInheritedWidget的实例(参见后面)

    • 最后,“updateShouldNotify”重写方法用于告诉InheritedWidget是否必须将通知传递给所有子窗口小部件(已注册/已订阅),如果对数据应用了修改(请参阅下文)。

      因此,我们需要将它放在树节点级别,如下所示:

      1. class MyParentWidget... {
      2. ...
      3. @override
      4. Widget build(BuildContext context){
      5. return new MyInheritedWidget(
      6. data: counter,
      7. child: new Row(
      8. children: <Widget>[
      9. ...
      10. ],
      11. ),
      12. );
      13. }
      14. }
  • 子child如何访问InheritedWidget的数据?
    在构建子child时,后者将获得对InheritedWidget的引用,如下所示:

    1. class MyChildWidget... {
    2. ...
    3.  
    4. @override
    5. Widget build(BuildContext context){
    6. final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
    7.  
    8. /// 从此刻开始,窗口小部件可以使用MyInheritedWidget公开的数据
    9. /// 通过调用:inheritedWidget.data
    10. return new Container(
    11. color: inheritedWidget.data.color,
    12. );
    13. }
    14. }

   请考虑以下显示窗口小部件树结构的图表。

    

为了说明一种交互方式,我们假设如下:

    • '小部件A’是一个将项目添加到购物车的按钮;
    • 小部件B”是一个显示购物车中商品数量的文本;
    • “小部件C”位于小部件B旁边,是一个内部带有任何文本的文本;
    • 我们希望“Widget B”在按下“Widget A”时自动在购物车中显示正确数量的项目,但我们不希望重建“Widget C”

InheritedWidget就是用来干这个的Widget!

    • 示例代码:

      1. class Item {
      2. String reference;
      3. Item(this.reference);
      4. }
      5.  
      6. class _MyInherited extends InheritedWidget {
      7. _MyInherited({
      8. Key key,
      9. @required Widget child,
      10. @required this.data,
      11. }) : super(key: key, child: child);
      12.  
      13. final MyInheritedWidgetState data;
      14.  
      15. @override
      16. bool updateShouldNotify(_MyInherited oldWidget) {
      17. return true;
      18. }
      19. }
      20.  
      21. class MyInheritedWidget extends StatefulWidget {
      22. MyInheritedWidget({
      23. Key key,
      24. this.child,
      25. }): super(key: key);
      26.  
      27. final Widget child;
      28.  
      29. @override
      30. MyInheritedWidgetState createState() => new MyInheritedWidgetState();
      31.  
      32. static MyInheritedWidgetState of(BuildContext context){
      33. return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
      34. }
      35. }
      36.  
      37. class MyInheritedWidgetState extends State<MyInheritedWidget>{
      38. List<Item> _items = <Item>[];
      39. int get itemsCount => _items.length;
      40. void addItem(String reference){
      41. setState((){
      42. _items.add(new Item(reference));
      43. });
      44. }
      45.  
      46. @override
      47. Widget build(BuildContext context){
      48. return new _MyInherited(
      49. data: this,
      50. child: widget.child,
      51. );
      52. }
      53. }
      54.  
      55. class MyTree extends StatefulWidget {
      56. @override
      57. _MyTreeState createState() => new _MyTreeState();
      58. }
      59.  
      60. class _MyTreeState extends State<MyTree> {
      61. @override
      62. Widget build(BuildContext context) {
      63. return new MyInheritedWidget(
      64. child: new Scaffold(
      65. appBar: new AppBar(
      66. title: new Text('Title'),
      67. ),
      68. body: new Column(
      69. children: <Widget>[
      70. new WidgetA(),
      71. new Container(
      72. child: new Row(
      73. children: <Widget>[
      74. new Icon(Icons.shopping_cart),
      75. new WidgetB(),
      76. new WidgetC(),
      77. ],
      78. ),
      79. ),
      80. ],
      81. ),
      82. ),
      83. );
      84. }
      85. }
      86.  
      87. class WidgetA extends StatelessWidget {
      88. @override
      89. Widget build(BuildContext context) {
      90. final MyInheritedWidgetState state = MyInheritedWidget.of(context);
      91. return new Container(
      92. child: new RaisedButton(
      93. child: new Text('Add Item'),
      94. onPressed: () {
      95. state.addItem('new item');
      96. },
      97. ),
      98. );
      99. }
      100. }
      101.  
      102. class WidgetB extends StatelessWidget {
      103. @override
      104. Widget build(BuildContext context) {
      105. final MyInheritedWidgetState state = MyInheritedWidget.of(context);
      106. return new Text('${state.itemsCount}');
      107. }
      108. }
      109.  
      110. class WidgetC extends StatelessWidget {
      111. @override
      112. Widget build(BuildContext context) {
      113. return new Text('I am Widget C');
      114. }
      115. }
    • 说明
      在这个非常基本的例子中,
      • _MyInherited是一个InheritedWidget,每次我们通过点击“Widget A”按钮添加一个Item时都会重新创建它
      • MyInheritedWidget是一个Widget,其状态包含Items列表。可以通过“(BuildContext context)的静态MyInheritedWidgetState”访问此状态。
      • MyInheritedWidgetState公开一个getter(itemsCount)和一个方法(addItem),以便它们可以被小部件使用,这是子小部件树的一部分
      • 每次我们向State添加一个Item时,MyInheritedWidgetState都会重建
      • MyTree类只是构建一个小部件树,将MyInheritedWidget作为树的父级
      • WidgetA是一个简单的RaisedButton,当按下它时,从最近的MyInheritedWidget调用addItem方法
      • WidgetB是一个简单的文本,显示最接近的MyInheritedWidget级别的项目数
  • 这一切如何运作
    • 注册Widget以供以后通知
      当子Widget调用MyInheritedWidget.of(context)时,它会调用MyInheritedWidget的以下方法,并传递自己的BuildContext。

      1. static MyInheritedWidgetState of(BuildContext context) {
      2. return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
      3. }

      在内部,除了简单地返回MyInheritedWidgetState的实例之外,它还将消费者窗口小部件订阅到更改通知。
      在场景后面,对这个静态方法的简单调用实际上做了两件事:

      • 当对InheritedWidget应用修改时,“consumer”窗口小部件会自动添加到将重建的订户列表中(此处为_MyInherited)
      • _MyInherited小部件(又名MyInheritedWidgetState)中引用的数据将返回给“使用者”
    • 过程
      由于’Widget A’和’Widget B’都已使用InheritedWidget订阅,因此如果对_MyInherited应用了修改,则当单击Widget A的RaisedButton时,操作流程如下(简化版本):
      • 调用MyInheritedWidgetState的addItem方法
      • MyInheritedWidgetState.addItem方法将新项添加到List
      • 调用setState()以重建MyInheritedWidget
      • 使用List 的新内容创建_MyInherited的新实例
      • _MyInherited记录在参数(数据)中传递的新State作为InheritedWidget,它检查是否需要“通知”“使用者”(答案为是)
      • 它迭代整个消费者列表(这里是Widget A和Widget B)并请求他们重建
      • 由于Wiget C不是消费者,因此不会重建。

但是,Widget A和Widget B都重建了,而重建Wiget A没用,因为它没有任何改变。如何防止这种情况发生?
在仍然访问“继承的”小组件时阻止某些小组件重建

Widget A也被重建的原因来自它访问MyInheritedWidgetState的方式。
正如我们之前看到的,调用context.inheritFromWidgetOfExactType()方法的实际上是自动将Widget订阅到“使用者”列表。

防止此自动订阅同时仍允许Widget A访问MyInheritedWidgetState的解决方案是更改MyInheritedWidget的静态方法,如下所示:

  1.       static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
  2.        return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
  3. : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
  4.      }

通过添加布尔类型的额外参数…

      • 如果“rebuild”参数为true(默认情况下),我们使用常规方法(并且Widget将添加到订阅者列表中)
      • 如果“rebuild”参数为false,我们仍然可以访问数据,但不使用InheritedWidget的内部实现

        因此,要完成解决方案,我们还需要稍微更新Widget A的代码,如下所示(我们添加false额外参数):

        1. class WidgetA extends StatelessWidget {
        2. @override
        3. Widget build(BuildContext context) {
        4. final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
        5. return new Container(
        6. child: new RaisedButton(
        7. child: new Text('Add Item'),
        8. onPressed: () {
        9. state.addItem('new item');
        10. },
        11. ),
        12. );
        13. }
        14. }

        在那里,当我们按下它时,Widget A不再重建。

 

【Flutter学习】之Widget数据共享之InheritedWidget的更多相关文章

  1. Flutter学习笔记(27)--数据共享(InheritedWidget)

    如需转载,请注明出处:Flutter学习笔记(27)--数据共享(InheritedWidget) InheritedWidget是Flutter中非常重要的一个功能型组件,它提供了一种数据在widg ...

  2. Flutter学习笔记(9)--组件Widget

    如需转载,请注明出处:Flutter学习笔记(9)--组件Widget 在Flutter中,所有的显示都是Widget,Widget是一切的基础,我们可以通过修改数据,再用setState设置数据(调 ...

  3. Flutter学习笔记(23)--多个子元素的布局Widget(Rwo、Column、Stack、IndexedStack、Table、Wrap)

    如需转载,请注明出处:Flutter学习笔记(23)--多个子元素的布局Widget(Rwo.Column.Stack.IndexedStack.Table.Wrap) 上一篇梳理了拥有单个子元素布局 ...

  4. Flutter学习笔记(22)--单个子元素的布局Widget(Container、Padding、Center、Align、FittedBox、Offstage、LimitedBox、OverflowBox、SizedBox)

    如需转载,请注明出处:Flutter学习笔记(22)--单个子元素的布局Widget(Container.Padding.Center.Align.FittedBox.Offstage.Limited ...

  5. Android程序员的Flutter学习笔记

    作为忠实与较资深的Android汪, 最近抽出了一些时间研究了一下Google的亲儿子Flutter, 尚属皮毛, 只能算是个简单的记录吧. Google自2017年第一次提出Flutter, 到20 ...

  6. Flutter学习笔记(8)--Dart面向对象

    如需转载,请注明出处:Flutter学习笔记(7)--Dart异常处理 Dart作为高级语言,支持面向对象的很多特性,并且支持基于mixin的继承方式,基于mixin的继承方式是指:一个类可以继承自多 ...

  7. Flutter学习笔记(10)--容器组件、图片组件

    如需转载,请注明出处:Flutter学习笔记(10)--容器组件.图片组件 上一篇Flutter学习笔记(9)--组件Widget我们说到了在Flutter中一个非常重要的理念"一切皆为组件 ...

  8. Flutter学习笔记(11)--文本组件、图标及按钮组件

    如需转载,请注明出处:Flutter学习笔记(10)--容器组件.图片组件 文本组件 文本组件(text)负责显示文本和定义显示样式,下表为text常见属性 Text组件属性及描述 属性名 类型 默认 ...

  9. Flutter学习笔记(12)--列表组件

    如需转载,请注明出处:Flutter学习笔记(12)--列表组件 在日常的产品项目需求中,经常会有列表展示类的需求,在Android中常用的做法是收集数据源,然后创建列表适配器Adapter,将数据源 ...

随机推荐

  1. SQL Server DBA日常检查常用SQL

    .数据库 --所有数据库的大小 exec sp_helpdb --所有数据库的状态 select name, user_access_desc, --用户访问模式 state_desc, --数据库状 ...

  2. php ucwords()函数 语法

    php ucwords()函数 语法 作用:把每个单词的首字符转换为大写 语法:ucwords(string) 参数: 参数 描述 string 必须,规定要转换的字符串 说明:把字符串中每个单词的首 ...

  3. java 标准输入输出流,打印流,数据流

    1 package stream; import static org.junit.Assert.assertNotNull; import java.io.BufferedReader; impor ...

  4. 【CF1238E】Keyboard Purchase(状压DP,贡献)

    题意:有m种小写字符,给定一个长为n的序列,定义编辑距离为序列中相邻两个字母位置差的绝对值之和,其中字母位置是一个1到m的排列 安排一种方案,求编辑距离最小 n<=1e5,m<=20 思路 ...

  5. [CSP-S模拟测试]:water(BFS)

    题目描述 有一块矩形土地被划分成$n\times m$个正方形小块.这些小块高低不平,每一小块都有自己的高度.水流可以由任意一块地流向周围四个方向的四块地中,但是不能直接流入对角相连的小块中.一场大雨 ...

  6. myeclipse2017使用总结

    1.之前的myeclipse 2010项目导入后,需要配置项目发布内容,否则class.lib.web.xml等文件不会自动发布到tomcat中:

  7. 网络安全随笔 - Linux的netstat查看端口 0.0.0.0与127.0.0.1的区别

    linux运维都需要对端口开放查看  netstat 就是对端口信息的查看 # netstat -nltp p 查看端口挂的程序 [root@iz2ze5is23zeo1ipvn65aiz ~]# n ...

  8. java知识点拾遗:)

    一篇有用的java基础知识总结http://www.cnblogs.com/xuwujing/p/8638329.html 枚举:http://blog.csdn.net/qq_27093465/ar ...

  9. [题解]Crazy Binary String-前缀和(2019牛客多校第三场B题)

    题目链接:https://ac.nowcoder.com/acm/contest/883/B 题意: 给你一段长度为n,且只有 ‘0’ 和 ‘1’ 组成的字符串 a[0,...,n-1].求子串中 ‘ ...

  10. CSS实现文字阴影的效果

    CSS中有两种阴影效果,一种是DropShadow(投影),另一种是Shadow(阴影).1.DropShadow语法:{FILTER:DropShadow(Color=color,OffX=offX ...