zoukankan      html  css  js  c++  java
  • 【Flutter学习】之Widget数据共享之InheritedWidget

    一,概述  

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

    二,源码分析

    • InheritedWidget
      先来看下InheritedWidget的源码:

      abstract class InheritedWidget extends ProxyWidget {
        const InheritedWidget({ Key key, Widget child })
          : super(key: key, child: child);
      
        @override
        InheritedElement createElement() => new InheritedElement(this);
      
        @protected
        bool updateShouldNotify(covariant InheritedWidget oldWidget);
      }

      它继承自ProxyWidget:

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

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

    • InheritedElement
      来看下InheritedElement源码

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

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

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

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

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

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

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

    三,示例demo

    • 自定义继承于InheritedWidget的类
      //模型数据
      class InheritedTestModel {
        final int count;
        const InheritedTestModel(this.count);
      }
      
      //自定义MyInheritedWidget(可以把它看成古代边城的哨所)
      class MyInheritedWidget extends InheritedWidget {
          //构造方法
          MyInheritedWidget({
            Key key,
            @required this.data, 
            @required Widget child //子组件
          }):super(key:key,child:child);  
      
          //属性
          final InheritedTestModel data;//共享的数据model
      
          //类方法
          static MyInheritedWidget of(BuildContext context){
            return context.inheritFromWidgetOfExactType(MyInheritedWidget);
          }
      
          //示例方法
        @override
        bool updateShouldNotify(MyInheritedWidget oldWidget) {
          // TODO: implement updateShouldNotify
          return data != oldWidget.data;
        }
      }

      代码解析

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

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

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

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

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

        class MyParentWidget... {
           ...
           @override
           Widget build(BuildContext context){
              return new MyInheritedWidget(
                 data: counter,
                 child: new Row(
                    children: <Widget>[
                       ...
                    ],
                 ),
              );
           }
        }
    • 子child如何访问InheritedWidget的数据?
      在构建子child时,后者将获得对InheritedWidget的引用,如下所示:

      class MyChildWidget... {
         ...
          
         @override
         Widget build(BuildContext context){
            final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
      
            /// 从此刻开始,窗口小部件可以使用MyInheritedWidget公开的数据
            /// 通过调用:inheritedWidget.data
            return new Container(
               color: inheritedWidget.data.color,
            );
         }
      }

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

        

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

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

    InheritedWidget就是用来干这个的Widget!

      • 示例代码:
        class Item {
           String reference;
           Item(this.reference);
        }
        
        class _MyInherited extends InheritedWidget {
          _MyInherited({
            Key key,
            @required Widget child,
            @required this.data,
          }) : super(key: key, child: child);
        
          final MyInheritedWidgetState data;
        
          @override
          bool updateShouldNotify(_MyInherited oldWidget) {
            return true;
          }
        }
        
        class MyInheritedWidget extends StatefulWidget {
          MyInheritedWidget({
            Key key,
            this.child,
          }): super(key: key);
        
          final Widget child;
        
          @override
          MyInheritedWidgetState createState() => new MyInheritedWidgetState();
        
          static MyInheritedWidgetState of(BuildContext context){
            return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
          }
        }
        
        class MyInheritedWidgetState extends State<MyInheritedWidget>{
          List<Item> _items = <Item>[];
          int get itemsCount => _items.length;
          void addItem(String reference){
            setState((){
              _items.add(new Item(reference));
            });
          }
        
          @override
          Widget build(BuildContext context){
            return new _MyInherited(
              data: this,
              child: widget.child,
            );
          }
        }
        
        class MyTree extends StatefulWidget {
          @override
          _MyTreeState createState() => new _MyTreeState();
        }
        
        class _MyTreeState extends State<MyTree> {
          @override
          Widget build(BuildContext context) {
            return new MyInheritedWidget(
              child: new Scaffold(
                appBar: new AppBar(
                  title: new Text('Title'),
                ),
                body: new Column(
                  children: <Widget>[
                    new WidgetA(),
                    new Container(
                      child: new Row(
                        children: <Widget>[
                          new Icon(Icons.shopping_cart),
                          new WidgetB(),
                          new WidgetC(),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            );
          }
        }
        
        class WidgetA extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            final MyInheritedWidgetState state = MyInheritedWidget.of(context);
            return new Container(
              child: new RaisedButton(
                child: new Text('Add Item'),
                onPressed: () {
                  state.addItem('new item');
                },
              ),
            );
          }
        }
        
        class WidgetB extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            final MyInheritedWidgetState state = MyInheritedWidget.of(context);
            return new Text('${state.itemsCount}');
          }
        }
        
        class WidgetC extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            return new Text('I am Widget C');
          }
        }
      • 说明
        在这个非常基本的例子中,
        • _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。

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

        在内部,除了简单地返回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的静态方法,如下所示:

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

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

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

          因此,要完成解决方案,我们还需要稍微更新Widget A的代码,如下所示(我们添加false额外参数):
          class WidgetA extends StatelessWidget {
            @override
            Widget build(BuildContext context) {
              final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
              return new Container(
                child: new RaisedButton(
                  child: new Text('Add Item'),
                  onPressed: () {
                    state.addItem('new item');
                  },
                ),
              );
            }
          }

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

     



  • 相关阅读:
    Eclipse的自动排版设置(format)
    Java中" "表示几个空格
    cookie和session详解
    IO流操作详解
    springmvc常用注解标签详解
    mavenWeb工程建立步骤
    数据导出为excel表格
    Springmvc jar包介绍
    【初级算法】5.只出现一次的数字
    【初级算法】4.存在重复
  • 原文地址:https://www.cnblogs.com/lxlx1798/p/11190230.html
Copyright © 2011-2022 走看看