一,概述
業務開發中經常會碰到這樣的情況,多個Widget需要同步同一份全局數據,比如點贊數、評論數、夜間模式等等。在安卓中,一般的實現方式是觀察者模式,需要開發者自行實現並維護觀察者的列表。在flutter中,原生提供了用於Widget間共享數據的InheritedWidget,當InheritedWidget發生變化時,它的子樹中所有依賴了它的數據的Widget都會進行rebuild,這使得開發者省去了維護數據同步邏輯的麻煩。
InheritedWidget是一個特殊的Widget,開發者可以將其作為另一個子樹的父級放在Widgets樹中。該子樹的所有小部件都必須能夠與該InheritedWidget公開的數據進行交互。
二,源碼分析
-
InheritedWidget
先來看下InheritedWidget的源碼:abstract class InheritedWidget extends ProxyWidget { const InheritedWidget({ Key key, Widget child }) : super(key: key, child: child); @override InheritedElement createElement() => new InheritedElement(this); @protected bool updateShouldNotify(covariant InheritedWidget oldWidget); }
它繼承自ProxyWidget:
abstract class ProxyWidget extends Widget { const ProxyWidget({ Key key, @required this.child }) : super(key: key); final Widget child; }
可以看出Widget內除了實現了
createElement
方法外沒有其他操作了,它的實現關鍵一定就是InheritedElement
了。
-
InheritedElement
來看下InheritedElement源碼class InheritedElement extends ProxyElement { InheritedElement(InheritedWidget widget) : super(widget); @override InheritedWidget get widget => super.widget; // 這個Set記錄了所有依賴的Element final Set<Element> _dependents = new HashSet<Element>(); // 該方法會在Element mount和activate方法中調用,_inheritedWidgets為基類Element中的成員,用於提高Widget查找父節點中的InheritedWidget的效率,它使用HashMap緩存了該節點的父節點中所有相關的InheritedElement,因此查找的時間復雜度為o(1) @override void _updateInheritance() { final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets; if (incomingWidgets != null) _inheritedWidgets = new HashMap<Type, InheritedElement>.from(incomingWidgets); else _inheritedWidgets = new HashMap<Type, InheritedElement>(); _inheritedWidgets[widget.runtimeType] = this; } // 該方法在父類ProxyElement中調用,看名字就知道是通知依賴方該進行更新了,這里首先會調用重寫的updateShouldNotify方法是否需要進行更新,然后遍歷_dependents列表並調用didChangeDependencies方法,該方法內會調用mardNeedsBuild,於是在下一幀繪制流程中,對應的Widget就會進行rebuild,界面也就進行了更新 @override void notifyClients(InheritedWidget oldWidget) { if (!widget.updateShouldNotify(oldWidget)) return; for (Element dependent in _dependents) { dependent.didChangeDependencies(); } } }
其中
_updateInheritance
方法在基類Element中的實現如下:void _updateInheritance() { _inheritedWidgets = _parent?._inheritedWidgets; }
總結來說就是Element在mount的過程中,如果不是InheritedElement,就簡單的將緩存指向父節點的緩存,如果是InheritedElement,就創建一個緩存的副本,然后將自身添加到該副本中,這樣做會有兩個值得注意的點:
- InheritedElement的父節點們是無法查找到自己的,即InheritedWidget的數據只能由父節點向子節點傳遞,反之不能。
- 如果某節點的父節點有不止一個同一類型的InheritedWidget,調用
inheritFromWidgetOfExactType
獲取到的是離自身最近的該類型的InheritedWidget。
- 看到這里似乎還有一個問題沒有解決,依賴它的Widget是在何時被添加到
_dependents
這個列表中的呢?
回憶一下從InheritedWidget中取數據的過程,對於InheritedWidget有一個通用的約定就是添加static的of方法,該方法中通過
inheritFromWidgetOfExactType
找到parent中對應類型的的InheritedWidget的實例並返回,與此同時,也將自己注冊到了依賴列表中,該方法的實現位於Element類,實現如下:@override InheritedWidget inheritFromWidgetOfExactType(Type targetType) { // 這里通過上述mount過程中建立的HashMap緩存找到對應類型的InheritedElement final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType]; if (ancestor != null) { // 這個列表記錄了當前Element依賴的所有InheritedElement,用於在當前Element deactivate時,將自己從InheritedElement的_dependents列表中移除,避免不必要的更新操作 _dependencies ??= new HashSet<InheritedElement>(); _dependencies.add(ancestor); // 這里將自己添加到了_dependents列表中,相當於注冊了監聽 ancestor._dependents.add(this); return ancestor.widget; } _hadUnsatisfiedDependencies = true; return null; }
三,示例demo
- 自定義繼承於InheritedWidget的類
//模型數據 class InheritedTestModel { final int count; const InheritedTestModel(this.count); } //自定義MyInheritedWidget(可以把它看成古代邊城的哨所) class MyInheritedWidget extends InheritedWidget { //構造方法 MyInheritedWidget({ Key key, @required this.data, @required Widget child //子組件 }):super(key:key,child:child); //屬性 final InheritedTestModel data;//共享的數據model //類方法 static MyInheritedWidget of(BuildContext context){ return context.inheritFromWidgetOfExactType(MyInheritedWidget); } //示例方法 @override bool updateShouldNotify(MyInheritedWidget oldWidget) { // TODO: implement updateShouldNotify return data != oldWidget.data; } }
代碼解析:
-
此代碼定義了一個名為“MyInheritedWidget”的Widget,旨在“共享”所有小部件(與子樹的一部分)中的某些數據(data)。
如前所述,為了能夠傳播/共享一些數據,需要將InheritedWidget定位在窗口小部件樹的頂部,這解釋了傳遞給InheritedWidget基礎構造函數的“@required Widget child”。
-
“Static MyInheritedWidget(BuildContext context)”方法允許所有子窗口小部件獲取最接近上下文的MyInheritedWidget的實例(參見后面)
-
最后,“updateShouldNotify”重寫方法用於告訴InheritedWidget是否必須將通知傳遞給所有子窗口小部件(已注冊/已訂閱),如果對數據應用了修改(請參閱下文)。
因此,我們需要將它放在樹節點級別,如下所示:
class MyParentWidget... { ... @override Widget build(BuildContext context){ return new MyInheritedWidget( data: counter, child: new Row( children: <Widget>[ ... ], ), ); } }
-
-
子child如何訪問InheritedWidget的數據?
在構建子child時,后者將獲得對InheritedWidget的引用,如下所示:class MyChildWidget... { ... @override Widget build(BuildContext context){ final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context); /// 從此刻開始,窗口小部件可以使用MyInheritedWidget公開的數據 /// 通過調用:inheritedWidget.data return new Container( color: inheritedWidget.data.color, ); } }
請考慮以下顯示窗口小部件樹結構的圖表。
為了說明一種交互方式,我們假設如下:
-
- '小部件A’是一個將項目添加到購物車的按鈕;
- 小部件B”是一個顯示購物車中商品數量的文本;
- “小部件C”位於小部件B旁邊,是一個內部帶有任何文本的文本;
- 我們希望“Widget B”在按下“Widget A”時自動在購物車中顯示正確數量的項目,但我們不希望重建“Widget C”
InheritedWidget就是用來干這個的Widget!
-
- 示例代碼:
class Item { String reference; Item(this.reference); } class _MyInherited extends InheritedWidget { _MyInherited({ Key key, @required Widget child, @required this.data, }) : super(key: key, child: child); final MyInheritedWidgetState data; @override bool updateShouldNotify(_MyInherited oldWidget) { return true; } } class MyInheritedWidget extends StatefulWidget { MyInheritedWidget({ Key key, this.child, }): super(key: key); final Widget child; @override MyInheritedWidgetState createState() => new MyInheritedWidgetState(); static MyInheritedWidgetState of(BuildContext context){ return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data; } } class MyInheritedWidgetState extends State<MyInheritedWidget>{ List<Item> _items = <Item>[]; int get itemsCount => _items.length; void addItem(String reference){ setState((){ _items.add(new Item(reference)); }); } @override Widget build(BuildContext context){ return new _MyInherited( data: this, child: widget.child, ); } } class MyTree extends StatefulWidget { @override _MyTreeState createState() => new _MyTreeState(); } class _MyTreeState extends State<MyTree> { @override Widget build(BuildContext context) { return new MyInheritedWidget( child: new Scaffold( appBar: new AppBar( title: new Text('Title'), ), body: new Column( children: <Widget>[ new WidgetA(), new Container( child: new Row( children: <Widget>[ new Icon(Icons.shopping_cart), new WidgetB(), new WidgetC(), ], ), ), ], ), ), ); } } class WidgetA extends StatelessWidget { @override Widget build(BuildContext context) { final MyInheritedWidgetState state = MyInheritedWidget.of(context); return new Container( child: new RaisedButton( child: new Text('Add Item'), onPressed: () { state.addItem('new item'); }, ), ); } } class WidgetB extends StatelessWidget { @override Widget build(BuildContext context) { final MyInheritedWidgetState state = MyInheritedWidget.of(context); return new Text('${state.itemsCount}'); } } class WidgetC extends StatelessWidget { @override Widget build(BuildContext context) { return new Text('I am Widget C'); } }
- 說明
在這個非常基本的例子中,
- 示例代碼:
-
-
- _MyInherited是一個InheritedWidget,每次我們通過點擊“Widget A”按鈕添加一個Item時都會重新創建它
- MyInheritedWidget是一個Widget,其狀態包含Items列表。可以通過“(BuildContext context)的靜態MyInheritedWidgetState”訪問此狀態。
- MyInheritedWidgetState公開一個getter(itemsCount)和一個方法(addItem),以便它們可以被小部件使用,這是子小部件樹的一部分
- 每次我們向State添加一個Item時,MyInheritedWidgetState都會重建
- MyTree類只是構建一個小部件樹,將MyInheritedWidget作為樹的父級
- WidgetA是一個簡單的RaisedButton,當按下它時,從最近的MyInheritedWidget調用addItem方法
- WidgetB是一個簡單的文本,顯示最接近的MyInheritedWidget級別的項目數
-
- 這一切如何運作?
-
注冊Widget以供以后通知
當子Widget調用MyInheritedWidget.of(context)時,它會調用MyInheritedWidget的以下方法,並傳遞自己的BuildContext。static MyInheritedWidgetState of(BuildContext context) { return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data; }
在內部,除了簡單地返回MyInheritedWidgetState的實例之外,它還將消費者窗口小部件訂閱到更改通知。
在場景后面,對這個靜態方法的簡單調用實際上做了兩件事:- 當對InheritedWidget應用修改時,“consumer”窗口小部件會自動添加到將重建的訂戶列表中(此處為_MyInherited)
- _MyInherited小部件(又名MyInheritedWidgetState)中引用的數據將返回給“使用者”
- 過程
由於’Widget A’和’Widget B’都已使用InheritedWidget訂閱,因此如果對_MyInherited應用了修改,則當單擊Widget A的RaisedButton時,操作流程如下(簡化版本):
- 調用MyInheritedWidgetState的addItem方法
- MyInheritedWidgetState.addItem方法將新項添加到List
- 調用setState()以重建MyInheritedWidget
- 使用List 的新內容創建_MyInherited的新實例
- _MyInherited記錄在參數(數據)中傳遞的新State作為InheritedWidget,它檢查是否需要“通知”“使用者”(答案為是)
- 它迭代整個消費者列表(這里是Widget A和Widget B)並請求他們重建
- 由於Wiget C不是消費者,因此不會重建。
-
但是,Widget A和Widget B都重建了,而重建Wiget A沒用,因為它沒有任何改變。如何防止這種情況發生?
在仍然訪問“繼承的”小組件時阻止某些小組件重建
Widget A也被重建的原因來自它訪問MyInheritedWidgetState的方式。
正如我們之前看到的,調用context.inheritFromWidgetOfExactType()
方法的實際上是自動將Widget訂閱到“使用者”列表。
防止此自動訂閱同時仍允許Widget A訪問MyInheritedWidgetState的解決方案是更改MyInheritedWidget的靜態方法,如下所示:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){ return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data; }
通過添加布爾類型的額外參數…
-
-
- 如果“rebuild”參數為true(默認情況下),我們使用常規方法(並且Widget將添加到訂閱者列表中)
- 如果“rebuild”參數為false,我們仍然可以訪問數據,但不使用InheritedWidget的內部實現
因此,要完成解決方案,我們還需要稍微更新Widget A的代碼,如下所示(我們添加false額外參數):
class WidgetA extends StatelessWidget { @override Widget build(BuildContext context) { final MyInheritedWidgetState state = MyInheritedWidget.of(context, false); return new Container( child: new RaisedButton( child: new Text('Add Item'), onPressed: () { state.addItem('new item'); }, ), ); } }
在那里,當我們按下它時,Widget A不再重建。
-