InheritedWidget 源碼分析和應用

目前,開發的flutter項目中,狀態管理庫使用是ProviderProvider 基於 InheritedWidget 組件封裝,想要減小平常開發採坑,就不得不去了解 InheritedWidget 組件的工做原理。因爲要從源碼角度分析 InheritedWidget 組件的工做原理,在閱讀本文前,最好對 flutter 的知識有必定了解,這樣才能更好的瞭解,本文所要表達的意思。android

  • 熟悉 flutter 基本使用。
  • 瞭解 provider 的框架。
  • 瞭解 WidgetElement 之間關係。
  • 瞭解 Elementflutter 渲染時方法的調用。

1、 InheritedWidget

本文中用到的生產環境安全

生產環境

1.1 InheritedWidget 簡述

/// 有效地沿樹向下傳播信息的 Widget 的基類,子 Widget 要想獲取最近特定類型的 InheritedWidget實例,請使用 
/// [BuildContext.dependOnInheritedWidgetOfExactType]。子 Widget 以這種方式引用時,當 InheritedWidget 改變狀態
/// 時,會從新構建依賴的子 Widget。
abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ Key key, Widget child })
      : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);

複製代碼

InheritedWidget 代碼很簡單,主要有兩個方法:markdown

  • createElement 生成 InheritedElement 對象。
  • updateShouldNotify 用於控制當前 InheritedWidget 發生變化, 所依賴的 Widget 是否須要重建。
1.1.1 BuildContext

InheritedWidget 描述可得知,若是子 Widget 須要獲取 InheritedWidget 對象,能夠經過 BuildContext.dependOnInheritedWidgetOfExactType 獲取。看下 BuildContext 類的 dependOnInheritedWidgetOfExactTypeapp

abstract class BuildContext {
  /// 獲取給定類型「T」的最近 widget,它必須是具體 [InheritedWidget] 子類的類型,並將此構建上下文註冊到該 widget,以便當該 widget 更改時(或引入該類型的新 widget, 或 widget 消失),此構建上下文將被重建,以便它能夠從該 widget 獲取新值。
   T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect });
  
  /// 獲取與給定類型「T」的最近 widget 對應的 element,該 element 必須是具體 [InheritedWidget] 子類的類型。 若是找不到這樣的 element,則返回 null。 
  /// 調用這個方法是 O(1) 一個小的常數因子。 此方法不會像 [dependOnInheritedWidgetOfExactType] 那樣與目標創建關
  /// 系。 不該從 [State.dispose] 調用此方法,由於此時 element 樹再也不穩定。 要從該方法引用祖先,請經過在 
  /// [State.didChangeDependencies] 中調用 [dependOnInheritedWidgetOfExactType] 來保存對祖先的引用。 使用 
  /// [State.deactivate] 中的此方法是安全的,每當 widget 從樹中移除時都會調用該方法。
  InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();
}
複製代碼

1.2 InheritedWidget 應用

接下來,咱們寫一個基於 InheritedWidget 組件實現的 計數器框架

1.2.1 CountScope
class CountScope extends InheritedWidget {
  const CountScope({this.count, Widget child}) : super(child: child);

  final int count;

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

  @override
  bool updateShouldNotify(CountScope oldWidget) {
    return oldWidget.count != count;
  }
}
複製代碼

CountScope 繼承 InheritedWidgetless

  1. of ,子 Widget 獲取 CountScope 對象。[見1.1小節]
  2. updateShouldNotifyoldWidget.count != count 刷新依賴的 widget[見1.1小節]
1.2.2 CountWidget
class CountWidget extends StatefulWidget {
  const CountWidget({Key key}) : super(key: key);

  @override
  _CountWidgetState createState() => _CountWidgetState();
}

class _CountWidgetState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print('_CountWidgetState build');
    final int count = CountScope.watch(context).count;
    return Container(child: Text('$count', style: Theme.of(context).textTheme.headline4));
  }
}
複製代碼

CountWidget 調用 CountScope.of(context).count 顯示計數結果。ide

1.2.3 MyHomePage
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountScope(count: _counter, child: CountWidget()),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
複製代碼

到這裏計數器功能差很少就完成了,當咱們點擊浮動 + 按鈕時,計數器的數值會累加,最後能夠看到效果就是這樣。函數

1.2.4 updateShouldNotify

咱們在 1.1 小節 提過, updateShouldNotify 返回 true ,表示更新依賴的 Widgetfalse 不更新,如今讓咱們驗證下。修改 CountScope 代碼,當 count < 3,才能更新依賴的 widget性能

class CountScope extends InheritedWidget {
  const CountScope({this.count, Widget child}) : super(child: child);

  final int count;

  static CountScope watch(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CountScope>();
  }

  @override
  bool updateShouldNotify(CountScope oldWidget) {
     //return oldWidget.count != count;
    print('count:$count');
    return count < 3;
  }
}
複製代碼

結果確實如咱們所料那樣,不停的點擊 + 按鈕,界面上的數字一直顯示的是 2。認真觀察 log 會發現,實際上 count 是會一直累加。優化

3.png

講到這裏,咱們就會有不少疑問?爲何 CountWidget 能獲取到 CountScopecount 的值?又爲何CountScopecount 數值有變化,可是當 count > 3 時,CountWidget 界面卻沒有更新呢?

遇事不決,看源碼

2、Widget 更新流程

咱們知道調用 setState 後,把當前 element 標記爲 dirty, 當下一次 vsync 信號到來的時候,回調執行 handleBeginFramehandleDrawFrame ,而後通過一些列的調用,最後會調用 BuildOwner.buildScope ,遍歷 _dirtyElements 集合,調用 Elementrebuild 刷新組件。

本文不會完整介紹 Widget 的更新機制,有興趣的同窗,能夠本身去了解下。(接下來源碼片斷,都只保留關鍵代碼)。

2.1 類圖

4.png

2.2 時序圖

5.png

2.3 Element#rebuild

前面,咱們提到調用 setState 後 ,通過一系列的調用,最終調用 Elementrebuild

abstract class Element extends DiagnosticableTree implements BuildContext {
  /// 當調用 [BuildOwner.scheduleBuildFor] 會將該 Element 標記爲 dirty,
  /// 當 Element 首次 build 時會被 [mount] 調用,當 widget 更改時由 [update] 調用。
  void rebuild() {
    if (!_active || !_dirty)
      return;
    performRebuild();
  }

  /// 在進行適當的檢查後由 rebuild() 調用。
  @protected
  void performRebuild(); /// [見2.4小節]
}
複製代碼

2.4 ComponentElement#performRebuild

abstract class ComponentElement extends Element {

  /// 調用 StatelessWidget 對象的 StatelessWidget.build 方法(對於無狀態小部件)
  /// 或 State 對象的 State.build 方法(對於有狀態小部件),而後更新 widget 樹。
  /// 在 mount 期間自動調用以生成第一個構建,並在 element 須要更新時經過 rebuild 調用。
  @override
  void performRebuild() {
    Widget built;
    try {
      built = build();  
    } finally {
      /// ...
      _dirty = false;
    }
    try {
      _child = updateChild(_child, built, slot); /// [見2.5小節]
    } catch (e, stack) {
      // ...
      _child = updateChild(null, built, slot);
    }

  /// 子類應該覆蓋這個函數來爲它們的小部件實際調用適當的 `build` 函數
  ///(例如,[StatelessWidget.build] 或 [State.build])。
  @protected
  Widget build();
}
複製代碼

2.5 Element#updateChild

abstract class Element extends DiagnosticableTree implements BuildContext {

  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget); /// [見2.6小節]
        newChild = child;
      } else {
        deactivateChild(child);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else { 
      newChild = inflateWidget(newWidget, newSlot); /// [見2.5.1小節]
    }
    return newChild;
  }
}
複製代碼
2.5.1 Element#inflateWidget

若是第一次調用 updateChild,默認 child = null,就會執行 inflateWidget,生成 Element newChild 對象,最後把 newChild 賦值給 _child [2.4小節],後續就使用 newChild 傳入 updateChild

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        // ...
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        return updatedChild;
      }
    }
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);// [見2.5.2小節]
    return newChild;
  }
複製代碼

調用 widget.createElement,生成 Element 對象,而後調用 Elementmount,遞歸調用,完成全部子 widget 的刷新。

2.5.2 Element#mount
void mount(Element parent, dynamic newSlot) {
    //...
    final Key key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    _updateInheritance();
  }
複製代碼
void _updateInheritance() {
    _inheritedWidgets = _parent?._inheritedWidgets;
  }
複製代碼

將父類中的 _inheritedWidgets 集合對象,傳到子類。

2.6 Element#update

[2.5.1] [2.5.2] 兩個小節,描述第一次加載 widget 過程。接下來介紹 child.update(newWidget)

@mustCallSuper
  void update(covariant Widget newWidget) { /// [見2.7小節]
    _widget = newWidget;
  }
複製代碼

經過查看 [2.1] 類圖,發現最下層的子類是 InheritedElement

2.7 InheritedElement#update

class InheritedElement extends ProxyElement {
	@override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget); /// [見2.8小節]
  }
}
複製代碼

widget.updateShouldNotify(oldWidget) 這個方法是否是似曾相識,這個就是一開始,咱們說的 InheritedWidget.updateShouldNotify [見1.2.4小節],這裏也驗證以前說法,若是 true 表示會繼續後續的,false 不執行後續的操做。咱們接着看 super.updated 後面的流程。

2.8 ProxyElement#update

經過查看 [2.1] 類圖, InheritedElement 繼承自 ProxyElement

abstract class ProxyElement extends ComponentElement {
  @override
  ProxyWidget get widget => super.widget as ProxyWidget;

  @override
  Widget build() => widget.child;
  
  /// ...
  @override
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget;
		/// ...
    updated(oldWidget);
    _dirty = true;
    rebuild();
  }
}
複製代碼
/// 當 widget 更改時, 在 build 期間調用。
  /// 默認狀況下,調用 notifyClients。 子類能夠覆蓋此方法以免沒必要要地調用 notifyClients(若是舊的和新的小部件是等效的)。
  @protected
  void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
  }
複製代碼
/// 通知其餘對象與此 Elment 關聯的 Widget 已更改。
  /// 在更改與此元素關聯的 Widget 以後但在重建此元素以前,在update期間(經過updated )調用
  @protected
  void notifyClients(covariant ProxyWidget oldWidget); // [見2.9小節]
複製代碼

2.9 InheritedElement#notifyClients

class InheritedElement extends ProxyElement {
  final Map<Element, Object> _dependents = HashMap<Element, Object>(); 
	
  /// [2.5.2小節] 初始化時調用
  @override
  void _updateInheritance() {
    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;
  }

  /// 返回使用 [setDependencies]爲 [dependent] 記錄的依賴項值。
  /// 每一個依賴元素都映射到單個對象值,該值表示元素如何依賴此 [InheritedElement]。
  /// 默認狀況下,此值爲 null,而且默認狀況下會無條件重建相關元素。
  /// 子類可使用 [updateDependencies] 管理這些值
  /// 以便他們能夠選擇性地重建 [notifyDependent] 中的依賴項。
  /// 此方法一般僅在 [updateDependencies] 的覆蓋中調用。
  /// 也能夠看看:
  /// [updateDependencies],每次使用 [dependOnInheritedWidgetOfExactType] 建立依賴項時都會調用它。
  /// [setDependencies],設置依賴元素的依賴值。
  /// [notifyDependent],能夠覆蓋它以使用依賴項的依賴項值來決定是否須要重建依賴項。
  /// [InheritedModel],這是一個使用該方法管理依賴值的類的例子。
  @protected
  Object getDependencies(Element dependent) {
    return _dependents[dependent];
  }

  /// 爲dependent設置getDependencies值返回的值。
  /// 每一個依賴元素都映射到單個對象值,該值表示元素如何依賴此InheritedElement 。 
  /// [updateDependencies]方法在默認狀況下將該值設置爲null,以便無條件地重建依賴元素。
  /// 子類可使用[updateDependencies]管理這些值,以便它們能夠有選擇地在[NotifyDependency]中重建依賴項。
  /// 此方法一般僅在updateDependencies覆蓋中調用。
  @protected
  void setDependencies(Element dependent, Object value) {
    _dependents[dependent] = value;
  }

  /// 添加新的 [dependent] 時由 [dependOnInheritedWidgetOfExactType] 調用。
  /// 每一個依賴元素均可以映射到單個對象值 [setDependencies]。 此方法可使用 [getDependencies] 查找現有依賴項。
  /// 默認狀況下,此方法將 [dependent] 的繼承依賴項設置爲 null。 這僅用於記錄對 [dependent] 的無條件依賴。
  /// 子類能夠管理本身的依賴項值,以便它們能夠在 [notifyDependent] 中重建依賴項。
  /// [getDependencies],返回依賴項的當前值元素。
  /// [setDependencies],設置依賴元素的值。
  /// [notifyDependent],能夠覆蓋它以使用依賴的依賴值來決定是否須要重建依賴。
  /// [InheritedModel],這是一個使用該方法的類的例子管理依賴值。
  @protected
  void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }

  /// 由 [notifyClients] 爲每一個依賴調用。
  /// 默認狀況下調用 [dependent.didChangeDependencies()] 。
	/// 子類能夠覆蓋此方法以根據 [getDependencies] 的值有選擇地調用 [didChangeDependencies] 。
	/// 也能夠看看:
	/// updateDependencies ,每次使用 [dependOnInheritedWidgetOfExactType] 建立依賴項時都會調用它。
	/// getDependencies ,它返回依賴元素的當前值。
	/// setDependencies ,它設置依賴元素的值。
	/// InheritedModel ,這是使用此方法管理依賴項值的類的示例。
  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies(); /// [見2.9.1]
  }

  /// 經過調用Element.didChangeDependencies通知全部依賴的Elements,這個widget已經更改。
  /// 此方法只能在 build 階段調用。 一般當 Inherited widget 被重建時,這個方法會自動調用,例如,做爲在 Inherited widget上方
  /// 調用State.setState的結果
  @override
  void notifyClients(InheritedWidget oldWidget) { 
    for (final Element dependent in _dependents.keys) {
      /// ...
      notifyDependent(oldWidget, dependent);
    }
  }
}
複製代碼
2.9.1 Element#didChangeDependencies
void didChangeDependencies() {
   markNeedsBuild();
}
複製代碼

看到 markNeedsBuild 是否是很熟悉。沒錯,就是咱們常用 setState,調用同樣的方法。

void setState(VoidCallback fn) {
    // ...
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }
複製代碼

上面咱們發現 notifyClients,遍歷循環 _dependents.keys,調用 Element.didChangeDependencies 更新依賴。咱們看下 dependents 是怎麼來的呢?咱們看 updateDependencies 註釋,是調用 context.dependOnInheritedWidgetOfExactType 添加 Element

2.10 Element#dependOnInheritedWidgetOfExactType

ElementBuildContext 具體實現類

@override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
複製代碼
@override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
複製代碼
2.11 流程圖

下圖,只是簡單畫了 InheritedElement 更新依賴組件的大體流程圖,方便你們吸取,具體的細節還須要本身去挖掘。

6.png

2.12 小結

dependOnInheritedWidgetOfExactType 經過 inheritedWidgets[T] 獲取到對應的 InheritedElement,當 InheritedElement不爲空,接着調用 InheritedElementupdateDependencies,把當前 Element 注入到要獲取 InheritedElement_dependents.keys 集合,接着返回 InheritedElementwidget 對象。當InheritedWidget 更新時,先經過 updateShouldNotify 判斷當前 InheritedElement 是否能進行 updated,當值爲 true 時,經過循環遍歷 _dependents.keys 集合,來更新全部依賴的 widget

3、自定義 Provider

前面寫了一個簡單的 計數器,同時也介紹了 InheritedWidget 更新依賴 widget 的原理分析。那咱們能不能基於 InheritedWidget 實現一個本身的 Provider 功能呢?接下來,咱們基於以前的 計數器 的代碼,一步一步優化,實現一個咱們本身的 Provider

3.1 優化方案(一)

經過上面咱們能發現,在主頁面直接調用 setState,是比較消耗性能,應該把 incrementCounter 操做剝離出去,單獨一個 model,這樣彷佛更符合實際的開發。

3.1.1 CountModel

新建 CountModel 用來進行數據處理及頁面更新操做,有點相似 androidviewModel

class CountModel with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void incrementCounter() {
    _count++;
    notifyListeners();
  }
}
複製代碼
3.1.2 CountProvider

初始化傳入數據源 T, 繼承 ChangeNotifier ,監聽 T notifyListeners 時,刷新 CountProvider,順便把 dependOnInheritedWidgetOfExactTypeCountScope 移到 CountProvider 類中。

typedef WidgetBuilder = Widget Function(BuildContext context);

class CountProvider<T extends ChangeNotifier> extends StatefulWidget {
  const CountProvider({
    Key key,
    this.value,
    this.builder,
  }) : super(key: key);

  final T value;

  final WidgetBuilder builder;

  static T of<T extends ChangeNotifier>(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CountScope<T>>().value;
  }

  @override
  _CountProviderState<T> createState() => _CountProviderState<T>();
}

class _CountProviderState<T extends ChangeNotifier> extends State<CountProvider<T>> {
  void _changeValue() {
    setState(() {});
  }

  @override
  void initState() {
    super.initState();
    widget.value?.addListener(_changeValue);
  }

  @override
  void dispose() {
    widget.value?.removeListener(_changeValue);
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant CountProvider<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.value != widget.value) {
      oldWidget.value?.removeListener(_changeValue);
    }
    widget.value?.addListener(_changeValue);
  }

  @override
  Widget build(BuildContext context) {
    print('_CountProviderState build');
    return CountScope<T>(
      value: widget.value,
      child: Builder(
        builder: (BuildContext context) {
          return widget.builder(context);
        },
      ),
    );
  }
}
複製代碼
3.1.3 CountScope
class CountScope<T extends ChangeNotifier> extends InheritedWidget {
  const CountScope({@required this.value, Widget child}) : super(child: child);

  final T value;

  @override
  bool updateShouldNotify(CountScope<T> oldWidget) {
    return true;
  }
}
複製代碼
3.1.4 CountWidget
class CountWidget extends StatefulWidget {
  const CountWidget({Key key}) : super(key: key);

  @override
  _CountWidgetState createState() => _CountWidgetState();
}

class _CountWidgetState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print('_CountWidgetState build');
    return Container(
      child: Text(
        '${CountProvider.of<CountModel>(context).count}',
        style: Theme.of(context).textTheme.headline4,
      ),
    );
  }
}
複製代碼
3.1.5 MyHomePage
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountProvider<CountModel>(
              value: CountModel(),
              builder: (BuildContext context) {
                return Column(children: [
                  const CountWidget(),
                  ClickButton(),
                ]);
              },
            ),
          ],
        ),
      ),
    );
  }
}
複製代碼
3.1.6 ClickButton
class ClickButton extends StatelessWidget {
  const ClickButton({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('ClickButton build');
    return ElevatedButton(
      onPressed: () {
        print('--- 點擊 ---');
        CountProvider.of<CountModel>(context).incrementCounter();
      },
      child: const Icon(Icons.add),
    );
  }
}
複製代碼

當咱們點擊 + 時,能正常更新數據,咱們看 log 會發現, MyHomePage 頁面沒有從新進行 build,若是咱們在MyHomePageColumn 組件裏面,再添加一個不依賴 CountProvider 的組件,會發生什麼?

3.1.7 Nothing
class Nothing extends StatefulWidget {
  const Nothing({Key key}) : super(key: key);

  @override
  _NothingState createState() => _NothingState();
}

class _NothingState extends State<Nothing> {
  @override
  Widget build(BuildContext context) {
    print('_NothingState build');
    return Container(
      child: const Text('我是一個不依賴的 widget'),
    );
  }
}
複製代碼
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountProvider<CountModel>(
              value: CountModel(),
              builder: (BuildContext context) {
                return Column(children: [
                  const CountWidget(),
                  Nothing(), /// 新增
                  ClickButton(),
                ]);
              },
            ),
          ],
        ),
      ),
    );
  }
}
複製代碼

結果以下圖:

能夠發現,每次咱們點擊 + ,調用 CountModel 進行 notifyListeners 更新時,都會致使 CountProvider 的子 Widget 所有刷新,不論是否依賴。

3.1.8 小結

如今咱們會發現有三個問題:

  1. 當前 Widget 調用 CountModel 方法時,能夠不進行依賴綁定?
  2. 若是咱們點擊 Flutter Hot Reload 按鈕,會發現計數器的 1,會變成 0,這合理嗎?
  3. 有沒有隻會更新依賴的 widget,不更新 InheritedElement 組件下,全部的子 widget ?

3.2 優化方案(二)

3.2.1 InheritedElement

由於咱們是在 CountProvider 中監聽 CountScope 的數據源變化,調用 setState 刷新整個 CountProvider,致使子 widget 也都所有更新,因此,咱們應該把數據源更新監聽移到 CountScope,讓 CountScope 通知刷新依賴的 widget 比較合理。

class CountScope<T extends ChangeNotifier> extends InheritedWidget {
  const CountScope({@required this.value, Widget child}) : super(child: child);

  final T value;

  @override
  bool updateShouldNotify(CountScope<T> oldWidget) {
    /// 由於是 InheritedElement 進行刷新,因此,這裏能夠設置爲 false
    return false;
  }

  @override
  CountScopeElement<T> createElement() {
    return CountScopeElement<T>(value, this);
  }
}

class CountScopeElement<T extends ChangeNotifier> extends InheritedElement {
  CountScopeElement(this.value, InheritedWidget widget) : super(widget) {
    value?.addListener(_handleUpdate);
  }

  final T value;

  void _handleUpdate() {
    markNeedsBuild();
  }

  @override
  void unmount() {
    value?.removeListener(_handleUpdate);
    super.unmount();
  }

  @override
  Widget build() {
    notifyClients(widget);
    return super.build();
  }
}
複製代碼

新建 CountScopeElement 繼承 InheritedElement,在 CountScopecreateElement 返回自定義 CountScopeElement 類。在 CountScopeElement 實現數據源變化監聽,在銷燬時調用移除監聽。

  1. _handleUpdate 調用 markNeedsBuild ,這個方法咱們應該都很熟悉,也就是 setState 調用後,調用一樣的方法,把 CountScopeElement 標記爲 dirtybuild 調用 notifyClients(widget) ,可能會人有不明白,爲何這裏要調用 notifyClients

  2. 經過 [2.8小節] 咱們能夠發現,當 CountScopeElement 進行 build 時,其實這裏的 child 沒有更新,仍是同一個對象。[見2.5小節],當 child.widget == newWidget 時,是不進行 child.update() 操做,也就沒有後續的一串依賴更新操做。因此,這裏要重寫 build ,手動調用內部的 notifyClients,通知依賴更新操做。

3.2.2 getElementForInheritedWidgetOfExactType

經過 [1.1小節] 咱們知道,獲取 InheritedWidgetdependOnInheritedWidgetOfExactType,並建立依賴關係。咱們經過 getElementForInheritedWidgetOfExactType 獲取 InheritedElement,這個過程,不建立依賴關係,彷佛能解決須要依賴的問題,咱們修改 CountProviderof 以下:

static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
    final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
    final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }
    return countScopeElement.value;
  }
複製代碼

實現思路,獲取到 CountScopeElement ,若是 listen = true 時,則進行依賴更新,最後返回 CountScopeElement.value

3.2.3 CountProvider
class CountProvider<T extends ChangeNotifier> extends StatefulWidget {
  const CountProvider({
    Key key,
    this.value,
    this.builder,
  }) : super(key: key);

  /// 數據源
  final T value;

  final WidgetBuilder builder;

  static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
    final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
    final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }
    return countScopeElement.value;
  }

  @override
  _CountProviderState<T> createState() => _CountProviderState<T>();
}

class _CountProviderState<T extends ChangeNotifier> extends State<CountProvider<T>> {

  @override
  Widget build(BuildContext context) {
    print('_CountProviderState build');
    return CountScope<T>(
      value: widget.value,
      child: Builder(
        builder: (BuildContext context) {
          return widget.builder(context);
        },
      ),
    );
  }
}
複製代碼

最終效果是可行的,也解決了 [3.1.8小節] 提出的三個問題。上面的代碼看起來彷佛沒什麼問題,若是 _MyHomePageState 在進行 setState 操做,會發現什麼事情?

class _MyHomePageState extends State<MyHomePage> {
  void _refresh() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountProvider<CountModel>(
              value: CountModel(),
              builder: (BuildContext context) {
                return Column(children: [
                  const CountWidget(),
                  Nothing(),
                  ElevatedButton(
                    onPressed: () {
                      print('--- 點擊 ---');
                      CountProvider.of<CountModel>(context, listen: false).incrementCounter();
                    },
                    child: const Icon(Icons.add),
                  ),
                ]);
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _refresh,
        tooltip: 'refresh',
        child: const Icon(Icons.refresh),
      ),
    );
  }
}
複製代碼

每次點擊 refresh,也會刷新 CountProvider 的依賴的子 widget ,這彷佛並無達到咱們的要求。那該怎麼解決呢?

3.3 優化(三)

3.3.1 InheritedBuildContext

抽象類, CountScopeElement 剝離出來須要用到功能和 value,方便後期擴展。

abstract class InheritedBuildContext<T> {
  void needsBuild();

  T get value;
}
複製代碼
3.3.2 Delegate

代理類 ,CountScopeElement 中方法的實現,value 的賦值。

abstract class Delegate<T> {
  CountScopeElement<T> element;

  T get value;
}
複製代碼
3.3.3 CountDelegate

Delegate 的實現類,主要實現了數據的監聽,通知 InheritedElement 刷新依賴。

class CountDelegate<T extends ChangeNotifier> extends Delegate<T> {
  CountDelegate({this.notifier});

  T notifier;

  @override
  T get value {
    notifier.addListener(() {
      element.needsBuild();
    });
    return notifier;
  }
}
複製代碼
3.3.4 CountScope
class CountScope<T> extends InheritedWidget {
  const CountScope({@required this.delegate, Widget child}) : super(child: child);

  final Delegate<T> delegate;

  @override
  bool updateShouldNotify(CountScope<T> oldWidget) {
    return false;
  }

  @override
  CountScopeElement<T> createElement() {
    return CountScopeElement<T>(delegate, this);
  }
}

class CountScopeElement<T> extends InheritedElement with InheritedBuildContext<T> {
  CountScopeElement(this.delegate, InheritedWidget widget) : super(widget);

  final Delegate<T> delegate;
  bool _dirty = false;

  @override
  CountScope<T> get widget => super.widget as CountScope<T>;

  @override
  void performRebuild() {
    delegate.element = this;
    super.performRebuild();
  }

  @override
  Widget build() {
    if (_dirty) {
      _dirty = false;
      notifyClients(widget);
    }
    return super.build();
  }

  @override
  void needsBuild() {
    _dirty = true;
    markNeedsBuild();
  }

  @override
  T get value => delegate.value;
}
複製代碼

主要修改,當須要從新 build 時,_dirty 標記爲 true,當 build 完成時,再設置爲 false,避免當CountScope 刷新,致使依賴的子 widget 也所有刷新。

3.3.5 CountProvider

集成 SingleChildStatelessWidget

class CountProvider<T extends ChangeNotifier> extends SingleChildStatelessWidget {
  CountProvider({Key key, this.value, this.builder})
      : _delegate = CountDelegate<T>(
          notifier: value,
        ),
        super(key: key);

  final T value;
  final WidgetBuilder builder;
  final Delegate<T> _delegate;

  static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
    final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
    final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }
    return countScopeElement.value;
  }

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    print('CountProvider buildWithChild');
    return CountScope<T>(delegate: _delegate, child: Builder(builder: builder));
  }
}
複製代碼

最終結果:當 CountScope 從新 rebuild 時,依賴的子 widget 不會跟着一塊兒 rebuild

3.3.5 小結

小結:這裏優化三個方案,也只是帶你們發現問題,一步一步去解決問題。其實,跟你們的業務結合的話,這上面的代碼,或許還會有不知足務求的地方。可是,無論什麼框架,也都是一步一步優化過來的。只要有瞭解決問題的思路和想法,我相信,任何問題都不在是問題。

4、 總結

前面分析 InheritedWidget 的源碼,調用流程等,在實際開發過程當中,咱們可能不會直接接觸 InheritedWidget ,可是,仍是能發現一些系統的組件,使用 InheritedWidget 進行封裝開發。例如:ThemeFocusButtonBarTheme 等,就不一一舉例了,你們能夠去看下源碼。寫這篇文章,一方面是爲了鞏固知識,方便往後查看、回憶知識點。另外一方便也是拋磚引玉,讓更多人瞭解 InheritedWidget,若是你們認真看,其實仍是會發現一些 provider 的影子。

若是文章有什麼不足的地方,歡迎批評指正,謝謝~

相關文章
相關標籤/搜索