FlutterDojo設計之道—狀態管理之路(四)

在Flutter中,跨Widget的數據共享,能夠以下圖這樣表示。android

當Child Widget想要跨Widget拿到其它Widget的數據時,一般就須要使用構造函數,將數據一層層傳遞到Child Widget,這顯然不是一個好的解決方案,不只讓Widget之間有了很大的耦合,也產生不少的冗餘數據。web

爲了解決這個問題,Flutter SDK提供了InheritedWidget這個Widget,InheritedWidget是除了StatefulWidget和StatelessWidget以外的第三個經常使用的Widget。當把InheritedWidget做爲Widget Tree的根節點時,這個Widget Tree就具備了一些新的功能,例如,Child Widget能夠根據BuildContext找到最近的指定類型的InheritedWidget,而不是經過Widget Tree的構造函數一層層進行傳遞,以下圖所示。微信

InheritedWidget的使用其實很是簡單,即共享數據給Child。因此它的核心點,其實就是兩個。less

  • 須要共享的數據
  • 從新updateShouldNotify的條件

經過BuildContext的dependOnInheritedWidgetOfExactType函數,就能夠直接獲取父Widget中的InheritedWidget。因此在InheritedWidget內部,一般會有一個of函數,用過調用BuildContext的dependOnInheritedWidgetOfExactType函數來獲取對應的父InheritedWidget。編輯器

只讀的InheritedWidget

InheritedWidget默認狀況下都是隻讀的,即只能將某個數據共享給Child Widget,而不能讓Child Widget對數據作更新。下面這個例子演示了一個最基本的InheritedWidget是如何共享數據的。ide

class InheritedWidgetReadOnlyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ReadOnlyRoot(
      count: 1008,
      child: ChildReadOnly(),
    );
  }
}

class ChildReadOnly extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build');
    ReadOnlyRoot root = ReadOnlyRoot.of(context);
    return Column(
      children: <Widget>[
        SubtitleWidget('InheritedWidget自己不具備寫數據的功能,須要結合State來獲取數據修改的能力'),
        Text(
          'show ${root.count}',
          style: TextStyle(fontSize: 20),
        ),
      ],
    );
  }
}

// 僅支持讀取屬性
class ReadOnlyRoot extends InheritedWidget {
  static ReadOnlyRoot of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<ReadOnlyRoot>();

  final int count;

  ReadOnlyRoot({
    Key key,
    @required this.count,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(ReadOnlyRoot oldWidget) => count != oldWidget.count;
}

給InheritedWidget增長讀寫功能

數據的狀態一般狀況下都是保存在StatefulWidget的State中的,因此,InheritedWidget必需要結合StatefulWidget才能具備修改數據的能力,所以,思路就是在InheritedWidget中持有一個StatefulWidget的State實例,同時,使用一個StatefulWidget,將本來的Child Widget之上,插入這個InheritedWidget,這樣就能夠藉助StatefulWidget來完成數據的修改能力,經過InheritedWidget來實現數據的共享能力。函數

class RootContainer extends StatefulWidget {
  final Widget child;

  RootContainer({
    Key key,
    this.child,
  }) : super(key: key);

  @override
  _RootContainerState createState() => _RootContainerState();

  static _RootContainerState of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<Root>().state;
}

class _RootContainerState extends State<RootContainer> {
  int count = 0;

  void incrementCounter() => setState(() => count++);

  @override
  Widget build(BuildContext context) {
    return Root(state: this, child: widget.child);
  }
}

// 同時支持讀取和寫入
class Root extends InheritedWidget {
  final _RootContainerState state;

  Root({
    Key key,
    @required this.state,
    @required Widget child,
  }) : super(key: key, child: child);

  // 判斷是否須要更新
  @override
  bool updateShouldNotify(Root oldWidget) => true;
}

在這種寫法中,InheritedWidget(Root)是在StatefulWidget(RootContainer)中初始化的,當使用StatefulWidget(RootContainer)的setState函數時,InheritedWidget(Root)重建了,可是其child並不會重建,由於它是widget.child,並不會由於State的重建而重建。flex

要注意的是,雖然這裏的StatefulWidget經過setState來修改數據了,但其子Widget並不會所有重繪,由於InheritedWidget的存在,Child Widget會有選擇性的進行重繪。ui

在這基礎上,使用就比較簡單了,代碼以下所示。this

class InheritedWidgetWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RootContainer(
      child: Column(
        children: <Widget>[
          Widget1(),
          Widget2(),
          Widget3(),
        ],
      ),
    );
  }
}

class Widget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build Widget1');
    return SubtitleWidget('InheritedWidget自己不具備寫數據的功能,須要結合State來獲取數據修改的能力');
  }
}

class Widget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build Widget2');
    return Text(
      'show ${RootContainer.of(context).count}',
      style: TextStyle(fontSize: 20),
    );
  }
}

class Widget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build Widget3');
    return RaisedButton(
      onPressed: () {
        RootContainer.of(context).incrementCounter();
      },
      child: Text('Add'),
    );
  }
}

相關代碼 Flutter Dojo-Widgets-Async-InheritedWidget

在上面這個Demo中,Widget二、3分別獲取和修改了InheritedWidget中的共享數據,實現了跨Widget的數據共享。

經過Log咱們能夠發現,初始化的時候,Widget一、二、3都執行了build,但點擊的時候,只有Widget二、3從新build了,可是Widget1並不會從新build。

這是什麼緣由呢?

其實這就是RootContainer.of(context)致使的。

當咱們執行RootContainer.of(context)這個函數的時候,實際上調用的是context.dependOnInheritedWidgetOfExactType函數,這個函數不只僅會返回指定類型的InheritedWidget,同時也會將Context對應的Widget添加到訂閱者列表中,也就是說,即便你調用這個函數,只是爲了執行某個函數,並非想刷新UI,可是系統依然認爲你須要刷新,從而致使Widget二、3都會執行rebuild。而Widget1,因爲沒有調用過of函數,因此不會被添加到訂閱者列表中,因此不會執行rebuild。

要想解決這個問題也很是簡單,那就是在不須要監聽的時候,使用findAncestorWidgetOfExactType便可,這個函數只會返回指定類型的Widget,而不會將監聽加入訂閱者列表中。

static _RootContainerState ofNoBuild(BuildContext context) => context.findAncestorWidgetOfExactType<Root>().state;

點擊按鈕的函數,只須要調用上面的這個函數,在點擊的時候,Widget3就不會執行rebuild了。

除了這種方式之外,還有一個方式,那就是經過const關鍵字,將一個Widget設置爲常量Widget,即不會發生改變,這個時候rebuild的時候,系統會發現const Widget並無發生改變,就不會rebuild了,這也是爲何在Flutter中,不少不須要改變的Padding、Margin、Theme、Size等參數須要儘量設置爲const的緣由,這樣能夠在rebuild的時候,提升效率。

在Flutter中,Theme的實現,就是採用的這種方式。

Widget Tree的遍歷

前面提到了兩種方式來獲取Widget Tree中的InheritedWidget,dependOnInheritedWidgetOfExactType和findAncestorWidgetOfExactType,從調用結果上來看,一種是會被加入訂閱者名單,一種只是單純的查找。

下面再來繼續仔細的看看這兩個函數的區別。

findAncestorWidgetOfExactType

首先來看下這個函數的註釋。

從中咱們能夠提取幾個關鍵信息。

  • 不會觸發rebuild
  • O(n)複雜度
  • 最好在didChangeDependencies中調用

因此findAncestorWidgetOfExactType有幾個比較經常使用的使用場景。

  • 在斷言中判斷父Widget的使用條件
  • 獲取父Widget對象,調用其方法

例如在一些Widget中,能夠經過Assert來判斷當前是否有使用該Widget的條件,例如Hero Widget。

dependOnInheritedWidgetOfExactType

首先也來看下這個函數的註釋。

  • 會觸發rebuild
  • O(1)複雜度
  • 最好在didChangeDependencies中調用

能夠發現,其實他跟findAncestorWidgetOfExactType是很是相似的,主要的區別仍是在因而否會rebuild,另外,dependOnInheritedWidgetOfExactType的效率很高。

項目地址 Flutter Dojo


本文分享自微信公衆號 - Android羣英傳(android_heroes)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索