簡介
業務開發中經常會碰到這樣的情況,多個Widget需要同步同一份全局數據,比如點贊數、評論數、夜間模式等等。在安卓中,一般的實現方式是觀察者模式,需要開發者自行實現并維護觀察者的列表。在flutter中,原生提供了用于Widget間共享數據的InheritedWidget,當InheritedWidget發生變化時,它的子樹中所有依賴了它的數據的Widget都會進行rebuild,這使得開發者省去了維護數據同步邏輯的麻煩。
使用范例
先來看下官方注釋中的范例:
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.inheritFromWidgetOfExactType(FrogColor);
}
@override
bool updateShouldNotify(FrogColor old) => color != old.color;
}
實現比較簡單,直接繼承InheritedWidget即可,類中的color即為需要共享的數據。
此外需要實現updateShouldNotify
方法,當InheritedWidget被更新時,該方法會被調用,并傳入更新之前的Widget對象,該方法內需要實現新舊數據的比較,若數據不同需要通知依賴的Widget更新,則返回true。
使用時,可以通過FrogColor.of(context).color
獲取到它的值。
下面舉一個具體的例子,簡單的使用場景如下圖所示:
將最上層的Widget用InheritedWidget包裝起來,并設置顏色為綠色,子Widget中需要使用該顏色時調用FrogColor.of(context).color
來獲取,如圖中的Icon和Image,此時調用該方法獲取到的顏色即為綠色。當InheritedWidget的數據有變化,即通過setState
將綠色改為紅色后,依賴了該數據的Icon和Image會自動rebuild,構造新的Widget時,此時從FrogColor.of(context).color
獲取到的顏色也會變為紅色,由此便實現了數據的同步。
源碼分析
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
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;
}
到此InheritedWidget就分析完了,相比觀察者模式,使用它是不是方便了很多呢?