InheritedWidget原理淺析

白瑞德發表於2020-07-29

定義和用法

本文只對InheritedWidget進行分析,需要讀者具備一些基礎知識,包括但不限於:

  1. Flutter的基本知識;
  2. Widget、Element的關係

一個可以高效的沿著樹高效傳遞資訊的基礎Widget。 使用者可使用BuildContext.dependOnInheritedWidgetOfExactType獲取最近特定型別的InheritedWidget例項,之後每當被引用的InheritedWidget自身狀態發生變化時,會導致引用者重新build。

按照官方教程,粗略的提一下用法(此處不再贅述詳細的用法,可參考資料共享(InheritedWidget)):

  • 首先定義一個InheritedWidget型別的Widget:
//官方Demo
class FrogColor extends InheritedWidget {
   const FrogColor({
     Key key,
     @required this.color,
     @required Widget child,
   }) : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

   final Color color;

   static FrogColor of(BuildContext context) {
     return context.dependOnInheritedWidgetOfExactType<FrogColor>();
   }

   @override
   bool updateShouldNotify(FrogColor old) => color != old.color;
}
複製程式碼
  • 將該Widget作為普通Widget新增到Widget樹中;
  • 在其任一子Widget中使用FrogColor.of(context).color即可獲得color資訊。

要想做到子Widget資料重新整理,有兩個關鍵點:

  1. 子Widget如何如何跨Widget拿到InheritedWidget
  2. InheritedWidget如何在資料改變時通知子Widget重新整理(build)

這兩點就是InheritedWidget的核心和關鍵所在,接下來我們對著兩個問題進行分析:

InheritedWidget在樹中的傳遞

在上面的示例程式碼中,子Widget中唯一可見和InheritedWidget發生關聯的程式碼就是FrogColor.of(context).color。其中的關鍵方法是dependOnInheritedWidgetOfExactType,該方法在BuildContext中定義,並在Element中有具體實現。檢視BuildContext.dependOnInheritedWidgetOfExactTypeElement中的原始碼如下:

Map<Type, InheritedElement> _inheritedWidgets;

@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({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;
}

複製程式碼

可以看到,每個Element例項都持有一個_inheritedWidgets,每當要為Widget新增特定型別的依賴時,就會從該集合裡取出相關型別的InheritedElement例項。那麼_inheritedWidgets是在何時儲存了InheritedElement的呢? 接下來繼續看Element原始碼,檢視_inheritedWidgets在何時賦值。 在element中有一個_updateInheritance方法:

void _updateInheritance() {
    assert(_active);
    _inheritedWidgets = _parent?._inheritedWidgets;
}
複製程式碼

它在mountactivate函式執行時被呼叫。也就是說每次element掛載和重新啟用時,element都會從它的上層element中打包拿到其所持有的所有InheritedElementInheritedElement繼承了Element,並重寫了_updateInheritance方法:

@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;
}
複製程式碼

相較於普通的ElementInheritedElement不僅會拿到其上層element所有的InheritedElement,而且會將自己也作為一個元素新增到集合中。 不難看出,每個Element都會持有一個集合,該集合持有其上層Element中出現的所有InheritedElement,每當出現一個InheritedElement,該InheritedElement會將其自身新增到該集合中,並向下傳遞。InheritedWidget正是通過這種方法確保下層控制元件可以訪問到其上層中所有的InheritedWidget

InheritedWidget如何進行重新整理

當InheritedWidget的狀態發生變化時,它是如何通知相關子樹進行重新整理呢,我們繼續分析dependOnInheritedWidgetOfExactType方法的程式碼,當從_inheritedWidgets中能取出特定型別的例項ancestor時。會執行dependOnInheritedElement方法:

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

InheritedElement例項會呼叫自己的updateDependencies方法並將當前Element例項傳遞過去:

final Map<Element, Object> _dependents = HashMap<Element, Object>();

@protected
void updateDependencies(Element dependent, Object aspect) {
  setDependencies(dependent, null);
}

@protected
void setDependencies(Element dependent, Object value) {
  _dependents[dependent] = value;
}
複製程式碼

InheritedElement中,通過_dependents集合儲存所有與該例項發生依賴的Widget。每次有Widget與InheritedElement建立依賴關係時,都會講自己新增到InheritedElement的集合中。而當InheritedElement更新時,會執行例項上的 notifyClients

@override
void notifyClients(InheritedWidget oldWidget) {
  assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
  for (final Element dependent in _dependents.keys) {
    assert(() {
      // check that it really is our descendant
      Element ancestor = dependent._parent;
      while (ancestor != this && ancestor != null)
        ancestor = ancestor._parent;
      return ancestor == this;
    }());
    // check that it really depends on us
    assert(dependent._dependencies.contains(this));
    //遍歷所有的element
    notifyDependent(oldWidget, dependent);
  }
}
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
  //依次呼叫所有子element的didChangeDependencies方法
  dependent.didChangeDependencies();
}
複製程式碼

在element的didChangeDependencies方法中會呼叫markNeedsBuild方法,該方法會將標記當前元素,並在下一幀到來時重新建立Widget,具體實現原理已經脫離InheritedWidget的知識範疇,這裡不再進行討論。

總結

InheritedWidget的關鍵核心是兩個集合。通過一個沿樹自上而下傳遞的集合儲存所有上層元件中的InheritedWidget,以便Widget可以拿到任意上層樹種的InheritedWidget。而在InheritedWidget則通過一個集合持有所有的依賴元素,每當有元素與其發生依賴關係,該元素就被被新增入集合中。當InheritedWidget的狀態發生改變時,將遍歷該集合並執行每個元素的didChangeDependencies方法。

相關文章