Flutter 之 InheritWidget 原始碼淺析

codelang發表於2021-03-24

InheritWidget 實現區域性重新整理必須注意到兩個點:

  • InheritWidget 下的子 widget 必須是快取過的
  • InheritWidget 下的子 widget 需新增進 _dependencies 集合中
  • InheritWidget 需要 rebuild

1、為什麼 InheritWidget 下的子 widget 必須是快取過的?

我們看下未做 widget 快取的例子:

class InheritWidgetState extends State<InheritWidgetDemo> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
        children: [
          ShareDataWidget(data: count, child: _TestWidget()),
          RaisedButton( child: Text("Increment"),onPressed: () => setState(() => ++count), // 觸發 rebuild
          )
        ]);}}

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

class  _TestWidgetState extends State<_TestWidget> {
    @override
    Widget build(BuildContext context) {  
      print("我被 build 了");
      return Text(ShareDataWidget.of(context,rebuild:true).data.toString());}}
複製程式碼
  • 如果通過日誌去檢視的話,你會發現沒有任何問題,單擊 Increment , print 確實打出了被 build 的日誌,你可能會覺得 _TestWidgetStaterebuild 設定為了true ,他就應該被 rebuild,沒有任何問題
  • 但當把 rebuild 設定為 false 呢?按照我們對 InheritWidget 的理解來看,_TestWidgetState不應該被觸發 build 操作,然而事實是,_TestWidgetState 仍然被觸發,主要原因是 widget 未快取,setState 會觸發 widget 以及子 widget 的 build 操作,然後每次都會重新構建 _TestWidget

2、 如何快取 widget

class InheritWidgetDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HomePage(
        child:  WidgetA()
    );
  }
}

class HomePage extends StatefulWidget {
      HomePage({Key key, this.child, }) : super(key: key);
      // 快取的 widget
      final Widget child;
      @override
      HomePageState createState() => HomePageState();
}

class HomePageState extends State<HomePage> {
 	 int counter = 0;
    void _incrementCounter() {
          setState(() {   ++counter;  });
     }
   @override 
   Widget build(BuildContext context) {
          print("home  ${widget.child.hashCode}"); // hashcode 一致
          return _MyInheritedWidget(
            data: this, child: widget.child,
   );}}
複製程式碼
  • 為什麼這麼寫可以?主要是 setState 觸發的是 HomePage 的 build,並不會影響到 InheritWidgetDemo ,又因為 widget 是在 InheritWidgetDemo 建立的,所以 HomePage 拿到的一直是沒有變化過的 widget,也可以通過列印 widget.hashCode 來驗證一下是否一致

3、InheritWidget 下的子 widget 需新增進 _dependencies 集合中

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
           _MyInheritedWidget.of(context,rebuild: true).data.counter.toString(),
        ),
    );
  }
}
複製程式碼
  • 如上是通過 InheritWidget 來實現資料展示,rebuild 為 true 即為 InheritWidget 發生資料改變時需要重新整理
static _MyInheritedWidget of(BuildContext context, {bool rebuild = true}) {
        if (rebuild) {
          return (context.dependOnInheritedWidgetOfExactType<_MyInheritedWidget>());
        }
        return (context.getElementForInheritedWidgetOfExactType<_MyInheritedWidget>()).widget;
  }
複製程式碼
  • rebuild 為 true 時,呼叫的是 dependOnInheritedWidgetOfExactType 方法
  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
        assert(ancestor != null);
        _dependencies ??= HashSet<InheritedElement>();
       // 將當前 element 新增進 _dependencies 集合中
        _dependencies!.add(ancestor);
        ancestor.updateDependencies(this, aspect);
        return ancestor.widget;
  }

  @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;
  }
複製程式碼
  • context 的實現類是 element ,最終會將當前 widget 的 element 快取到 _dependencies 集合中

4、如何觸發區域性重新整理

class HomePageState extends State<HomePage> {
 	 int counter = 0;
    void _incrementCounter() {
          setState(() {   ++counter;  });
     }
   @override 
   Widget build(BuildContext context) {
          return _MyInheritedWidget(
            data: this, child: widget.child,
   );}}
複製程式碼
  • 在呼叫 _incrementCounter 方法觸發 setState 時,會觸發 HomePageState 的 rebuild 操作,從而影響到子 widget 的 _MyInheritedWidget rebuild,也就是 InheritWidget
abstract class ProxyElement extends ComponentElement {
   ....
     @override
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget;
    ...
    updated(oldWidget);
    _dirty = true;
    rebuild();
  }
複製程式碼
  • InheritWidget 的 element 是 InheritElement ,繼承的是 ProxyElement,最終會走到 update 方法中來更新當前的 widget,可以直接看 updated 方法
-> 父類 ProxyElement  
@protected
  void updated(covariant ProxyWidget oldWidget) {
     notifyClients(oldWidget);
  }

-> 子類 InheritElement
  @override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
 }  
複製程式碼
  • 子類實現了父類的 updated 方法,子類會對 InheritWidget 的 updateShouldNotify 進行判斷,如果返回 true ,則子 wdiget 會呼叫 didChangeDependencies 方法
  • 父類 updated 會觸發 notifyClients 方法,notifyClients 是交由子類 InheritElement 來實現的
@override
void notifyClients(InheritedWidget oldWidget) {
    assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
    for (final Element dependent in _dependents.keys) {
      ....
      // check that it really depends on us
      assert(dependent._dependencies!.contains(this));
      notifyDependent(oldWidget, dependent);
    }
  }
}
複製程式碼
  • notifyClients 會遍歷 _dependencies 集合的 element 元素,並判斷 element 是否包含當前 rebuild 的 InheritWidget , 然後呼叫 notifyDependent 方法
  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }
複製程式碼
  • notifyDependent 會呼叫 element 的 didChangeDependencies 方法
  @mustCallSuper
  void didChangeDependencies() {
    assert(_lifecycleState == _ElementLifecycle.active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();// build
  }
複製程式碼
  • 進入到 didChangeDependencies 方法中檢視發現,element 會觸發 markNeedsBuild 進行重新構建,也就是區域性 widget 重新整理

5、如何使區域性重新整理失效

1、InheritWidget 的 updateShouldNotify 實現類返回 false

2、不將 widget 新增到 _dependencies 集合中,可以採用 getElementForInheritedWidgetOfExactType

相關文章