Widget基礎系列 - InheritedWidget

在Flutter中,Widget可以說是第一基礎概念。Widget是對用戶界面的不可變描述,可被膨化為管理底層渲染樹的Element。

理解Widget原理是掌握Flutter編程至關重要的一步,本系列主要介紹Widget的基礎知識,本文是第三篇:

  • StatelessWidget
  • StatefulWidget
  • InheritedWidget
  • Key

簡介

前面講過了StatelessWidget和StatefulWidget,當我們的App變得越來越大、Widget樹越來越復雜時,傳遞和訪問數(shù)據(jù)會變得非常繁瑣。

widget_tree

假設每個Widget嵌套了四、五個Widget,我們希望在最底層的Widget訪問頂層Widget的數(shù)據(jù),如果只使用StatelessWidget和StatefulWidget,我們不得不將數(shù)據(jù)添加在每個Widget的構造函數(shù)里,如果需要添加、修改、刪除數(shù)據(jù),那簡直就是噩夢,一串構造函數(shù)都需要修改。你應該已經猜到了,InheritedWidget就是解決這個問題的。

widget_tree_inherited

當我們將InheritedWidget添加到Widget樹中時,在其下面的所有Widget都可以得到一個指向它的引用,因此我們稱其為Inherited Widget。

假設有一個InheritedWidget的子類:InheritedAppState:

class InheritedAppState extends InheritedWidget {
  final AppState appState;
  
  InheritedAppState({this.appState, Widget child}): super(child: child);
  
  @override
  bool updateShouldNotify(_InheritedStateContainer old) => true;
  
  static InheritedAppState of(BuildContext context) =>
    context.inheritFromWidgetOfExactType(InheritedAppState);
}

在子孫Widget中,可以直接通過context.inheritFromWidgetOfExactType(InheritedAppState)獲取InheritedAppState對象。為了提升代碼簡潔性和易讀性,InheritedWidget一般會包含一個靜態(tài)的of方法。

我們注意到,InheritedWidget是不可變的,因此上面的appState屬性是final類型的。當InheritedWidget的數(shù)據(jù)改變時,我們只能重建整個widget。請謹記這一點,所有的Widget都是不可變的,不要改變widget中保存的數(shù)據(jù)。

final僅表示屬性不能被再次賦值,不代表內部不能改變。比如,我們可以將附加的數(shù)據(jù)模型改為一個service對象,這個service可以訪問數(shù)據(jù)庫、網(wǎng)絡請求、本地assets資源等。

class InheritedAppState extends InheritedWidget {
  final AppStateService service;
  
  InheritedAppState({this.service, Widget child}): super(child: child);
  
  @override
  bool updateShouldNotify(_InheritedStateContainer old) => true;
  
  static InheritedAppState of(BuildContext context) =>
    context.inheritFromWidgetOfExactType(InheritedAppState);
}

簡單來說,InheritedWidget提供了一種在widget樹中從上到下傳遞、共享數(shù)據(jù)的方式。Flutter SDK正是通過InheritedWidget來共享應用主題和Locale等信息的。

原理

對于InheritedWidget來說,需要實現(xiàn):

  • 在子孫widget中可以方便地獲取到InheritedWidget或者保存在InheritedWidget中的數(shù)據(jù)
  • InheritedWidget中的數(shù)據(jù)改變時,更新所有依賴的子孫widget

下面我們分別介紹實現(xiàn)原理。

獲取InheritedWidget對象

我們已經知道,通過context.inheritFromWidgetOfExactType可以在子孫widget中獲取上層的InheritedWidget對象,具體是如何實現(xiàn)的呢?

BuildContext實際上就是Element,其相關實現(xiàn)代碼如下:

Map<Type, InheritedElement> _inheritedWidgets;

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

  @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

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

可以看到,在Element中,有一個Map類型的屬性_inheritedWidgets,鍵是Type,而值是InheritedElement。當我們調用inheritFromWidgetOfExactType方法時,直接從這個Map中查找對應類型的InheritedElement,并調用其updateDependencies方法注冊依賴,然后返回其關聯(lián)的widget對象。

那么_inheritedWidgets的值從哪兒來呢?從parent那兒來。那parent的值又從哪兒來呢?從距離最近的InheritedElement祖先那兒來。InheritedElement中的_inheritedWidgets又是如何維護的呢?

@override
  void _updateInheritance() {
    assert(_active);
    final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    _inheritedWidgets[widget.runtimeType] = this;
  }

很簡單,獲取parent中的值,并注冊自身。

因此,一個普通Element中的_inheritedWidgets,保存的就是其所有InheritedElement祖先節(jié)點的信息,通過這個屬性,我們就可以方便地、高效率地獲取到樹上層的InheritedWidget了。

更新子孫widget

這個相對簡單,不涉及新的知識,當上層的InheritedWidget變化時,其widget子樹會重建,子孫widget在其build方法中就可以獲取到新的值了。

為了確保子孫widget中的數(shù)據(jù)能及時更新,只應該在build方法、layout和paint回調以及State.didChangeDependencies方法中調用inheritFromWidgetOfExactType方法。

不應該在widget的構造方法或者State.initState方法中調用inheritFromWidgetOfExactType方法,因為當數(shù)據(jù)變更時,這些方法不會被再次調用。也不應該在State.dispose方法中調用,因為此時element樹是不穩(wěn)定的。如果必須要在State.dispose中訪問上層的InheritedWidget,可以提前在State.didChangeDependencies方法中保存一個引用。在State.deactivate方法中調用是安全的,此方法在widget從樹中移除時被調用 。

如果我們希望在InheritedWidget更新時做一些處理怎么辦?比如從網(wǎng)絡獲取數(shù)據(jù),如果放在build中代價顯然太高了,從設計模式的角度來說也極其不合理,build方法最好只負責構建widget樹。

State有一個didChangeDependencies方法,當其依賴的對象發(fā)生改變時會被調用,最主要的場景就是InheritedWidget依賴。此方法在initState方法結束之后也會被馬上調用一次,可以在這個方法中安全地調用BuildContext.inheritFromWidgetOfExactType方法。子類很少需要重寫此方法,因為框架總會在依賴改變時調用build方法。只有當需要執(zhí)行網(wǎng)絡請求這樣的昂貴操作時,才需要重寫此方法。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容