【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不再重建。

 

 

 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM