# 使用InheritedWidget傳遞數據

  • 2019 年 10 月 6 日
  • 筆記

# 使用InheritedWidget傳遞數據

除了StatefulWidget、StatelessWidget之外flutter還提供了另外一個用的Widget組件即InheritedWidget。那麼該組件的作用時什麼呢?
# 我們來看一下數據是如何從父widget傳遞到子widget的

下面我們定義一個嵌套三層的數據傳遞例子:

class DataTransferAWidget extends StatelessWidget {    final int data;    DataTransferAWidget(this.data);    @override    Widget build(BuildContext context) {      return Container(        alignment: Alignment.center,        child: Column(          mainAxisAlignment: MainAxisAlignment.center,          children: <Widget>[            Expanded(child: Text("A:${data.toString()}")),            Expanded(              //子組件依賴自己的data數據              child: DataTransferBWidget(data),            )          ],        ),      );    }  }    class DataTransferBWidget extends StatelessWidget {    final int data;    DataTransferBWidget(this.data);    @override    Widget build(BuildContext context) {      return Container(          alignment: Alignment.center,          child: Column(              mainAxisAlignment: MainAxisAlignment.spaceBetween,              children: <Widget>[                Expanded(child: Text("B:${data.toString()}")),                //子組件依賴自己的data數據                Expanded(child: DataTransferCWidget(data))              ]));    }  }    class DataTransferCWidget extends StatelessWidget {    final int data;    DataTransferCWidget(this.data);    @override    Widget build(BuildContext context) {      return Container(          alignment: Alignment.center, child: Text("C:${data.toString()}"));    }  }    class DataTransferDemoPage extends StatefulWidget {    @override    State<StatefulWidget> createState() {      return _DataTransferDemoPageState();    }  }    class _DataTransferDemoPageState extends State<DataTransferDemoPage> {    //定義一個待向子widget傳遞的數據    int data = 0;    @override    Widget build(BuildContext context) {      return Scaffold(        appBar: AppBar(          title: Text("數據傳遞"),        ),        body: Container(            alignment: Alignment.center,            //向子傳遞data            child: DataTransferAWidget(data)),        floatingActionButton: FloatingActionButton(            child: Icon(Icons.add),            onPressed: () {              setState(() {                data++;              });            }),      );    }

上面例子你看到,每個DataTransferWidget的構造函數都依賴父widget的data,如果還有第4層,第5層…等嵌套的話,data要不停的通過構造函數傳遞,甚是麻煩。

既然每層widget依賴的都是根的data,那麼為什麼不定義一個全局靜態變數來做呢?(好想法,我們試一下)

# 使用static代替構造函數傳遞數據
typedef ChildWidgetBuilder =DataTransferCWidget Function(int);  class DataTransferAWidget extends StatelessWidget {    //下面程式碼已經沒有意義了    //final int data;    //DataTransferAWidget(this.data);    @override    Widget build(BuildContext context) {      return Container(        alignment: Alignment.center,        child: Column(          mainAxisAlignment: MainAxisAlignment.center,          children: <Widget>[            Expanded(child: Text("A:${DataModel.data.toString()}")),            Expanded(              child: DataTransferBWidget(),            )          ],        ),      );    }  }    class DataTransferBWidget extends StatelessWidget {    //下面程式碼已經沒有意義了    //final int data;    //DataTransferBWidget(this.data);    @override    Widget build(BuildContext context) {      return Container(          alignment: Alignment.center,          child: Column(              mainAxisAlignment: MainAxisAlignment.spaceBetween,              children: <Widget>[                Expanded(child: Text("B:${DataModel.data.toString()}")),                Expanded(child: DataTransferCWidget())              ]));    }  }    class DataTransferCWidget extends StatelessWidget {    //下面程式碼已經沒有意義了    //final int data;    //DataTransferCWidget(this.data);    @override    Widget build(BuildContext context) {      return Container(          alignment: Alignment.center, child: Text("C:${DataModel.data.toString()}"));    }  }  //靜態變數  class DataModel{    static int data=0;  }  class DataTransferDemoPage extends StatefulWidget {      @override    State<StatefulWidget> createState() {      return _DataTransferDemoPageState();    }  }    class _DataTransferDemoPageState extends State<DataTransferDemoPage> {    @override    Widget build(BuildContext context) {      return Scaffold(        appBar: AppBar(          title: Text("數據傳遞"),        ),        body: Container(            alignment: Alignment.center, child: DataTransferAWidget()),        floatingActionButton: FloatingActionButton(            child: Icon(Icons.add),            onPressed: () {              setState(() {                DataModel.data++;              });            }),      );    }  }

全局static類型的變數是不是更方便一些呢?!

# 該是InheritedWidget出場的時候了

flutter官方api是這樣說的:有效地在樹中傳播資訊的小部件的基類,下面咱們來看一下它的定義:

//我們可以看到該類是一個抽象類  abstract class InheritedWidget {    //這是一個常量構造函數,常量的作用就是一旦編譯後就不可能再被修改    const InheritedWidget({ Key key, Widget child })      : super(key: key, child: child);      //這個函數不在我們的討論範圍之內    @override    InheritedElement createElement() => InheritedElement(this);    //這個函數可以接收舊的對象,用來判斷新舊InheritedWidget是否一樣,它又什麼作用呢?往下看...    @protected    bool updateShouldNotify(covariant InheritedWidget oldWidget);  }

它是一個抽象類,這就意味著我們要去實現它:

class TestModel {    int model = 0;    void add() {      model++;    }  }    class MyInheriteWidget extends InheritedWidget {    //這裡我們定義一個我們準備向後代傳遞的數據    final DataModel model;    //另外InheriteWidget需要包裹一個Widget    //由於InheritedWidget僅用來傳輸數據,    //所以這個child或者child的後代需要是一個StatelessWidget或StatefulWidget    final Widget child;      MyInheriteWidget({Key key, @required this.model, @required this.child})        : super(key: key, child: child);      //這裡我們要判斷新舊widget是否相同    @override    bool updateShouldNotify(MyInheriteWidget oldWidget) {      return model != oldWidget.model;    }      static TestModel of(BuildContext context, [bool onChangeBuild = true]) {      var widget = onChangeBuild          ? context.inheritFromWidgetOfExactType(MyInheriteWidget)          : context              .ancestorInheritedElementForWidgetOfExactType(MyInheriteWidget)              .widget;      return (widget as MyInheriteWidget).model;    }  }

此時,一個可以用與向後台傳遞數據的InheritedWidget就完成了,下面來看如何使用:

class _InheriteWidgetDemoPage extends State<InheriteWidgetDemoPage> {    //在一個有狀態的widget中定義它需要維護的狀態    var testModel = TestModel();      @override    Widget build(BuildContext context) {      //使用InheritedWidget向後代傳遞數據      return MyInheriteWidget(        model: testModel,        child: Scaffold(            appBar: AppBar(title: Text("title")),            //或許你想這樣使用,這是錯誤的,我們應該獲取的是InheritedWidget的數據而不是當前widget的            //body:Text(testModel.model.toString())            //為了更清楚,我們將獲取InheritedWidget的數據放在一個單獨的widget中執行,如下            body: TestAWidget(),            floatingActionButton: Builder(              builder: (context) => FloatingActionButton(                child: Icon(Icons.add),                onPressed: () {                  setState(() {                    MyInheriteWidget.of(context).add();                  });                },              ),            )),      );    }  }    class TestAWidget extends StatelessWidget {    TestAWidget();    @override    Widget build(BuildContext context) {     	//官方文檔提到我們應該這樣獲取InheritedWidget上傳遞的數據,是不是有點類似全局靜態變數呢?!      //不同的是 這是從context獲取的,因為context可以貫穿整個widget依賴樹,像android的context      //官方給的demo不是下面這樣      //var myInheritedWidget = context.inheritFromWidgetOfExactType(MyInheriteWidget) as MyInheriteWidget;      //return Center(child: Text(myInheritedWidget.model.model.toString()));      //應該是這樣,將獲取傳遞對象的程式碼放在MyInheritedWidget中,這僅是為了程式碼可讀性     var model = MyInheriteWidget.of(context).model.toString();      return Center(child: Text(model));    }  }

此時InheritedWidget向後代傳遞數據的方式已經完了,從現有的實現來說並沒有比全局靜態變數有什麼優略之處,如果僅是這樣也就沒有它存在的價值了,請往下看…

# updateShouldNotify的作用

要弄清楚updateShouldNotify的作用我們還要翻一翻源碼了,不然只看api文檔還是一頭霧水,上源碼:

//我們知道一個widget由:widget,element,ReanderObject三部分組成  class InheritedElement extends ProxyElement {        ...        @override      void updated(InheritedWidget oldWidget) {          //通過字元串搜索你會找到該方法是在此處調用的          if (widget.updateShouldNotify(oldWidget))              super.updated(oldWidget);      }        @override      void notifyClients(InheritedWidget oldWidget) {        		...              notifyDependent(oldWidget, dependent);          }      }        @protected      void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {          dependent.didChangeDependencies();      }      ...  }    class StatefulElement extends ComponentElement {    @override    void didChangeDependencies() {      super.didChangeDependencies();      //調了這麼一圈最後是為了調用didChangeDependencies方法      _state.didChangeDependencies();    }  }    abstract class ProxyElement extends ComponentElement{      @protected      void updated(covariant ProxyWidget oldWidget) {          notifyClients(oldWidget);      }  }

從上面重要的源碼可以看到最後updateShouldNotify決定了didChangeDependencie()會不會被調用。

另外,分析源碼的調用過程,也可以藉助idea的debugger模式,在Frames裡面我們可以看到程式碼是如何一步步被調用的。

# didChangeDependencie又是什麼

didChangeDependencie是State中定義的一個回調函數,而State正是暴漏StatefulWidget生命周期的地方,我們可以同步實現State的不同方法,來對widget的生命周期變化時做出一些響應。

上面例子我們定義了一個無狀態的TestAWidget來演示如果獲取InheritedWidget要向子傳遞的數據,下面我們通過一個有狀態的控制項來展示在獲取數據的同時響應didChangeDependencie的調用。

lass TestBWidget extends StatefulWidget {      @override    State<StatefulWidget> createState() {      return _TestBWidgetState();    }  }    class _TestBWidgetState extends State<TestBWidget> {    //這裡就是要被調用的地方    @override    void didChangeDependencies() {      super.didChangeDependencies();      print('TestBWidget');    }      @override    Widget build(BuildContext context) {      var model = MyInheriteWidget.of(context).model.toString();      return Center(child: Text(model));    }  }

現在我們已經知道了updateShouldNotify返回true時didChangeDependencies()就會被調用。

InheritedWidget是為了向後代傳遞數據,如果InheritedWidget發生了嵌套呢?可能你只想響應某一個先輩的didChangeDependencies()調用,這也是可以,通過ancestorInheritedElementForWidgetOfExactType來獲取先輩的數據,不會將自己的didChangeDependencies註冊進該先輩的調用集合。

# 有選擇性的註冊didChangeDependencies調用

前面的程式碼中有一段是這樣的:

  static TestModel of(BuildContext context, [bool onChangeBuild = true]) {      //通過一個變數來控制調用哪個方法獲取先輩的數據      var widget = onChangeBuild          ? context.inheritFromWidgetOfExactType(MyInheriteWidget)          : context              .ancestorInheritedElementForWidgetOfExactType(MyInheriteWidget)              .widget;      return (widget as MyInheriteWidget).model;    }

要搞清楚inheritFromWidgetOfExactType與ancestorInheritedElementForWidgetOfExactType的區別,我認為看源程式碼是最直接的:

...  @override    InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {        final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];      //重點再這裡      if (ancestor != null) {        return inheritFromElement(ancestor, aspect: aspect);      }      _hadUnsatisfiedDependencies = true;      return null;    }      @override    InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {        final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];      return ancestor;    }      @override    InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {      _dependencies ??= HashSet<InheritedElement>();        //將widget添加到依賴集合後就會收到didChangeDependencies調用      _dependencies.add(ancestor);      ancestor.updateDependencies(this, aspect);      return ancestor.widget;    }  ...

下面配上一張流程圖來加深一下印象:

# 總結

  1. InheritedWidget用與向後代傳遞/共享數據
  2. 通過updateShouldNotify方法可以控制是否要調用後代的didChangeDependencies方法
  3. didChangeDependencies被調用的前提是這個後代是一個StatefulWidget,且是通過inheritFromWidgetOfExactType方法來獲取先輩的數據。