Flutter實戰(zhàn) 數(shù)據(jù)共享(InheritedWidget)

2021-03-08 11:38 更新

InheritedWidget是 Flutter 中非常重要的一個功能型組件,它提供了一種數(shù)據(jù)在 widget 樹中從上到下傳遞、共享的方式,比如我們在應用的根 widget 中通過InheritedWidget共享了一個數(shù)據(jù),那么我們便可以在任意子 widget 中來獲取該共享的數(shù)據(jù)!這個特性在一些需要在 widget 樹中共享數(shù)據(jù)的場景中非常方便!如 Flutter SDK 中正是通過 InheritedWidget 來共享應用主題(Theme)和 Locale (當前語言環(huán)境)信息的。

InheritedWidget和 React 中的 context 功能類似,和逐級傳遞數(shù)據(jù)相比,它們能實現(xiàn)組件跨級傳遞數(shù)據(jù)。InheritedWidget的在 widget 樹中數(shù)據(jù)傳遞方向是從上到下的,這和通知Notification(將在下一章中介紹)的傳遞方向正好相反。

#didChangeDependencies

在之前介紹StatefulWidget時,我們提到State對象有一個didChangeDependencies回調(diào),它會在“依賴”發(fā)生變化時被 Flutter Framework 調(diào)用。而這個“依賴”指的就是子 widget 是否使用了父 widget 中InheritedWidget的數(shù)據(jù)!如果使用了,則代表子 widget 依賴有依賴InheritedWidget;如果沒有使用則代表沒有依賴。這種機制可以使子組件在所依賴的InheritedWidget變化時來更新自身!比如當主題、locale(語言)等發(fā)生變化時,依賴其的子 widget 的didChangeDependencies方法將會被調(diào)用。

下面我們看一下之前“計數(shù)器”示例應用程序的InheritedWidget版本。需要說明的是,本示例主要是為了演示InheritedWidget的功能特性,并不是計數(shù)器的推薦實現(xiàn)方式。

首先,我們通過繼承InheritedWidget,將當前計數(shù)器點擊次數(shù)保存在ShareDataWidgetdata屬性中:

class ShareDataWidget extends InheritedWidget {
  ShareDataWidget({
    @required this.data,
    Widget child
  }) :super(child: child);

    
  final int data; //需要在子樹中共享的數(shù)據(jù),保存點擊次數(shù)

    
  //定義一個便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)  
  static ShareDataWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  }


  //該回調(diào)決定當data發(fā)生變化時,是否通知子樹中依賴data的Widget  
  @override
  bool updateShouldNotify(ShareDataWidget old) {
    //如果返回true,則子樹中依賴(build函數(shù)中有調(diào)用)本widget
    //的子widget的`state.didChangeDependencies`會被調(diào)用
    return old.data != data;
  }
}

然后我們實現(xiàn)一個子組件_TestWidget,在其build方法中引用ShareDataWidget中的數(shù)據(jù)。同時,在其didChangeDependencies() 回調(diào)中打印日志:

class _TestWidget extends StatefulWidget {
  @override
  __TestWidgetState createState() => new __TestWidgetState();
}


class __TestWidgetState extends State<_TestWidget> {
  @override
  Widget build(BuildContext context) {
    //使用InheritedWidget中的共享數(shù)據(jù)
    return Text(ShareDataWidget
        .of(context)
        .data
        .toString());
  }


  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    //父或祖先widget中的InheritedWidget改變(updateShouldNotify返回true)時會被調(diào)用。
    //如果build中沒有依賴InheritedWidget,則此回調(diào)不會被調(diào)用。
    print("Dependencies change");
  }
}

最后,我們創(chuàng)建一個按鈕,每點擊一次,就將ShareDataWidget的值自增:

class InheritedWidgetTestRoute extends StatefulWidget {
  @override
  _InheritedWidgetTestRouteState createState() => new _InheritedWidgetTestRouteState();
}


class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
  int count = 0;


  @override
  Widget build(BuildContext context) {
    return  Center(
      child: ShareDataWidget( //使用ShareDataWidget
        data: count,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(bottom: 20.0),
              child: _TestWidget(),//子widget中依賴ShareDataWidget
            ),
            RaisedButton(
              child: Text("Increment"),
              //每點擊一次,將count自增,然后重新build,ShareDataWidget的data將被更新  
              onPressed: () => setState(() => ++count),
            )
          ],
        ),
      ),
    );
  }
}

運行后界面如圖7-1所示:

圖7-1

每點擊一次按鈕,計數(shù)器就會自增,控制臺就會打印一句日志:

I/flutter ( 8513): Dependencies change

可見依賴發(fā)生變化后,其didChangeDependencies()會被調(diào)用。但是讀者要注意,如果_TestWidget的build 方法中沒有使用 ShareDataWidget 的數(shù)據(jù),那么它的didChangeDependencies()將不會被調(diào)用,因為它并沒有依賴 ShareDataWidget。例如,我們將__TestWidgetState代碼改為下面這樣,didChangeDependencies()將不會被調(diào)用:

class __TestWidgetState extends State<_TestWidget> {
  @override
  Widget build(BuildContext context) {
    // 使用InheritedWidget中的共享數(shù)據(jù)
    //    return Text(ShareDataWidget
    //        .of(context)
    //        .data
    //        .toString());
     return Text("text");
  }


  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // build方法中沒有依賴InheritedWidget,此回調(diào)不會被調(diào)用。
    print("Dependencies change");
  }
}

上面的代碼中,我們將build()方法中依賴ShareDataWidget的代碼注釋掉了,然后返回一個固定Text,這樣一來,當點擊 Increment 按鈕后,ShareDataWidgetdata雖然發(fā)生變化,但由于__TestWidgetState并未依賴ShareDataWidget,所以__TestWidgetStatedidChangeDependencies方法不會被調(diào)用。其實,這個機制很好理解,因為在數(shù)據(jù)發(fā)生變化時只對使用該數(shù)據(jù)的 Widget 更新是合理并且性能友好的。

思考題:Flutter framework 是怎么知道子 widget 有沒有依賴 InheritedWidget 的?

#應該在didChangeDependencies()中做什么?

一般來說,子 widget 很少會重寫此方法,因為在依賴改變后 framework 也都會調(diào)用build()方法。但是,如果你需要在依賴改變后執(zhí)行一些昂貴的操作,比如網(wǎng)絡請求,這時最好的方式就是在此方法中執(zhí)行,這樣可以避免每次build()都執(zhí)行這些昂貴操作。

#深入了解InheritedWidget

現(xiàn)在來思考一下,如果我們只想在__TestWidgetState中引用ShareDataWidget數(shù)據(jù),但卻不希望在ShareDataWidget發(fā)生變化時調(diào)用__TestWidgetStatedidChangeDependencies()方法應該怎么辦?其實答案很簡單,我們只需要將ShareDataWidget.of()的實現(xiàn)改一下即可:

//定義一個便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)
static ShareDataWidget of(BuildContext context) {
  //return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}

唯一的改動就是獲取ShareDataWidget對象的方式,把dependOnInheritedWidgetOfExactType()方法換成了context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget,那么他們到底有什么區(qū)別呢,我們看一下這兩個方法的源碼(實現(xiàn)代碼在Element類中,ContextElement的關系我們將在后面專門介紹):

@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  return ancestor;
}
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  //多出的部分
  if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

我們可以看到,dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()多調(diào)了dependOnInheritedElement方法,dependOnInheritedElement源碼如下:

  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

可以看到dependOnInheritedElement方法中主要是注冊了依賴關系!看到這里也就清晰了,調(diào)用dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()的區(qū)別就是前者會注冊依賴關系,而后者不會,所以在調(diào)用dependOnInheritedWidgetOfExactType()時,InheritedWidget和依賴它的子孫組件關系便完成了注冊,之后當InheritedWidget發(fā)生變化時,就會更新依賴它的子孫組件,也就是會調(diào)這些子孫組件的didChangeDependencies()方法和build()方法。而當調(diào)用的是 getElementForInheritedWidgetOfExactType()時,由于沒有注冊依賴關系,所以之后當InheritedWidget發(fā)生變化時,就不會更新相應的子孫 Widget。

注意,如果將上面示例中ShareDataWidget.of()方法實現(xiàn)改成調(diào)用getElementForInheritedWidgetOfExactType(),運行示例后,點擊"Increment"按鈕,會發(fā)現(xiàn)__TestWidgetStatedidChangeDependencies()方法確實不會再被調(diào)用,但是其build()仍然會被調(diào)用!造成這個的原因其實是,點擊"Increment"按鈕后,會調(diào)用_InheritedWidgetTestRouteStatesetState()方法,此時會重新構建整個頁面,由于示例中,__TestWidget 并沒有任何緩存,所以它也都會被重新構建,所以也會調(diào)用build()方法。

那么,現(xiàn)在就帶來了一個問題:實際上,我們只想更新子樹中依賴了ShareDataWidget的組件,而現(xiàn)在只要調(diào)用_InheritedWidgetTestRouteStatesetState()方法,所有子節(jié)點都會被重新build,這很沒必要,那么有什么辦法可以避免呢?答案是緩存!一個簡單的做法就是通過封裝一個StatefulWidget,將子 Widget 樹緩存起來,具體做法下一節(jié)我們將通過實現(xiàn)一個Provider Widget 來演示如何緩存,以及如何利用InheritedWidget 來實現(xiàn) Flutter 全局狀態(tài)共享。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號