1 前言
在Flutter中,由於其激進式組合(Everything is Widget)的設計理念,多層Widget是很常見的,如何在不同層的Widget之間傳遞資料,是開發者經常遇到的問題。通常做法是將資料新增到下級Widget的建構函式中。但是如果傳遞資料的Widget層級相隔較遠,那將資料一層層傳下來將是個噩夢。因此,Flutter提供了一種在Widget樹中自上而下傳遞資料的方式:InheritedWidget。本文將深入講解InheritedWidget的原理,並舉例說明InheritedWidget如何使用。
2 核心函式
- updateShouldNotify
updateShouldNotify用於控制依賴於該InheritedWidget的元件是否需要重建。如果updateShouldNotify的值是true,則當InheritedWidget發生變化時,依賴於該InheritedWidget的Widget會被rebuild,其Element的didChangeDependencies函式會被呼叫,從而更新子Widget中的顯示。反之,則不會重建依賴於該InheritedWidget的Widget。
流程圖如下:
可以看到,InheritedWidget變化時,如果updateShouldNotify是true,會通過notifyClients呼叫子元件的didChangeDependencies函式,從而呼叫markNeedsBuild,將本Element加入_dirtyElements列表中。大家都知道,_dirtyElements中儲存的是需要重建的Element,會在下一幀時被rebuild,因此在下一幀子元件會被重建(rebuild)。
InheritedElement的updated原始碼如下:
@override
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget);
}
複製程式碼
Element的didChangeDependencies原始碼如下:
void didChangeDependencies() {
assert(_active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}
複製程式碼
3 核心流程
- 儲存InheritedWidget
既然InheritedWidget能用於讓使用者快速從子元件獲取,那必然會涉及到一個問題,它是如何傳遞給子元件的呢?其實Flutter Framework也是將InheritedWidget一層層傳遞下來的,只不過由於Framework層自行處理了,因此這個過程對於我們是透明的。我們現在來梳理下InheritedWidget傳遞的過程。
Element中,有一個map:_inheritedWidgets。儲存了所有上級節點中的InheritedElement。其原始碼如下:
Map<Type, InheritedElement> _inheritedWidgets;
複製程式碼
其中,key中Type是InheritedWidget的子類,value是InheritedElement。為什麼這裡value儲存的是InheritedElement而不是InheritedWidget呢?由以前的文章可以知道Element中儲存著對應Widget的引用,因此可以通過InheritedElement獲取對應的InheritedWidget。而且Widget在上級Widget重建時會被重建,因此儲存InheritedElement更合適。
在普通的Element中,_inheritedWidgets會直接複製其父元件中_inheritedWidgets的值,其原始碼如下:
void _updateInheritance() {
assert(_active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
複製程式碼
而在InheritedElement中,_inheritedWidgets會首先複製其父元件中_inheritedWidgets的值,然後將自己新增進列表,其原始碼如下:
@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;
}
複製程式碼
由此可以看出,InheritedElement就是這樣一層層傳遞下來的。_inheritedWidgets賦值流程如下:
由該流程圖可以看出,_inheritedWidgets在Element被加入Element Tree時就已經被賦值,因此其在子元件的build函式中是可以訪問得到的。
- 獲取InheritedWidget
我們已經知道了InheritedElement會傳遞到下級元件中,那怎麼獲取它呢?Flutter提供了專門獲取某個InheritedWidget型別的函式dependOnInheritedWidgetOfExactType.其原始碼如下:
@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;
}
複製程式碼
由第三行可以看出,此函式會從_inheritedWidgets中尋找對應的InheritedElement,並返回其InheritedWidget。
除了dependOnInheritedWidgetOfExactType,Flutter還提供了另一個專門獲取某個InheritedWidget型別的函式:getElementForInheritedWidgetOfExactType。其原始碼如下:
@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
複製程式碼
對比其與dependOnInheritedWidgetOfExactType原始碼,可以看到dependOnInheritedWidgetOfExactType多了dependOnInheritedElement函式的呼叫,該函式用於建立InheritedWidget和呼叫dependOnInheritedWidgetOfExactType的元件的依賴關係。其有兩個步驟:
- 將依賴的InheritedElement加入本Element的_dependencies列表,該列表中儲存了本Element所有依賴的InheritedElement.
- 將本Element加入依賴的InheritedElement的_dependents map,該列表中儲存了所有依賴該InheritedElement的Element。
如果使用的是dependOnInheritedWidgetOfExactType,則當被依賴的InheritedWidget被更新時,依賴的子元件會被rebuild;而使用的是getElementForInheritedWidgetOfExactType時,由於不會建立相應的依賴關係,InheritedWidget被更新時,依賴的子元件不會被rebuild。
4 示例
下面是一個使用示例。
首先,我們通過繼承InheritedWidget,將當前計數器點選次數儲存在ShareDataWidget的data屬性中:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
@required this.data,
Widget child
}) :super(child: child);
final int data; //需要在子樹中共享的資料,儲存點選次數
//定義一個便捷方法,方便子樹中的widget獲取共享資料
static ShareDataWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
}
//該回撥決定當data發生變化時,是否通知子樹中依賴data的Widget
@override
bool updateShouldNotify(ShareDataWidget old) {
//如果返回true,則子樹中依賴(build函式中有呼叫)本widget
//的子widget的`state.didChangeDependencies`會被呼叫
return old.data != data;
}
}
複製程式碼
然後我們實現一個子元件_TestWidget,在其build方法中引用ShareDataWidget中的資料。同時,在其didChangeDependencies回撥中列印日誌:
class _TestWidget extends StatefulWidget {
@override
__TestWidgetState createState() => new __TestWidgetState();
}
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
print("__TestWidgetState build");
//使用InheritedWidget中的共享資料
return Text(ShareDataWidget
.of(context)
.data
.toString());
// return Text("tex");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//父或祖先widget中的InheritedWidget改變(updateShouldNotify返回true)時會被呼叫。
//如果build中沒有依賴InheritedWidget,則此回撥不會被呼叫。
print("Dependencies change");
}
}
複製程式碼
最後,我們建立一個按鈕,每點選一次,就將ShareDataWidget的值自增:
class InheritedWidgetTestRoute extends StatefulWidget {
@override
_InheritedWidgetTestRouteState createState() => new _InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget( //使用ShareDataWidget
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_TestWidget(),
RaisedButton(
child: Text("Increment"),
//每點選一次,將count自增,然後重新build,ShareDataWidget的data將被更新
onPressed: () => setState(() => ++count),
)
],
),
),
);
}
}
複製程式碼
5 小結
本文主要介紹了InheritedWidget的核心函式和核心流程,並展示了一個InheritedWidget的示例。其重點如下:
- InheritedWidget通過updateShouldNotify函式控制依賴其的子元件是否在InheritedWidget變化時會被重建:如果updateShouldNotify返回true,InheritedWidget變化時子元件的build會被呼叫,反之則不會。
- InheritedWidget是儲存在inheritedWidgets中被層層傳遞到子元件中。
- 在子元件中可以通過dependOnInheritedWidgetOfExactType或者getElementForInheritedWidgetOfExactType獲取上級元件中對應的InheritedWidget物件,其中dependOnInheritedWidgetOfExactType會建立依賴關係,因此InheritedWidget變化時,子元件的build會被呼叫,getElementForInheritedWidgetOfExactType不會建立依賴關係,InheritedWidget變化時,子元件的build不會被呼叫。
6 參考文件
Flutter框架分析(一)--架構總覽
Flutter框架分析(二)-- Widget
Flutter框架分析(三)-- Element
Flutter框架分析- Parent Data