InheritedWidget
是 Flutter 中非常重要的一個功能型組件,它提供了一種數(shù)據(jù)在 widget 樹中從上到下傳遞、共享的方式,比如我們在應用的根 widget 中通過InheritedWidget
共享了一個數(shù)據(jù),那么我們便可以在任意子 widget 中來獲取該共享的數(shù)據(jù)!這個特性在一些需要在 widget 樹中共享數(shù)據(jù)的場景中非常方便!如 Flutter SDK 中正是通過 InheritedWidget 來共享應用主題(Theme
)和 Locale (當前語言環(huán)境)信息的。
InheritedWidget
和 React 中的 context 功能類似,和逐級傳遞數(shù)據(jù)相比,它們能實現(xiàn)組件跨級傳遞數(shù)據(jù)。InheritedWidget
的在 widget 樹中數(shù)據(jù)傳遞方向是從上到下的,這和通知Notification
(將在下一章中介紹)的傳遞方向正好相反。
在之前介紹StatefulWidget
時,我們提到State
對象有一個didChangeDependencies
回調(diào),它會在“依賴”發(fā)生變化時被 Flutter Framework 調(diào)用。而這個“依賴”指的就是子 widget 是否使用了父 widget 中InheritedWidget
的數(shù)據(jù)!如果使用了,則代表子 widget 依賴有依賴InheritedWidget
;如果沒有使用則代表沒有依賴。這種機制可以使子組件在所依賴的InheritedWidget
變化時來更新自身!比如當主題、locale(語言)等發(fā)生變化時,依賴其的子 widget 的didChangeDependencies
方法將會被調(diào)用。
下面我們看一下之前“計數(shù)器”示例應用程序的InheritedWidget
版本。需要說明的是,本示例主要是為了演示InheritedWidget
的功能特性,并不是計數(shù)器的推薦實現(xiàn)方式。
首先,我們通過繼承InheritedWidget
,將當前計數(shù)器點擊次數(shù)保存在ShareDataWidget
的data
屬性中:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
@required this.data,
Widget child
}) :super(child: child);
final int data; //需要在子樹中共享的數(shù)據(jù),保存點擊次數(shù)
//定義一個便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)
static ShareDataWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
}
//該回調(diào)決定當data發(fā)生變化時,是否通知子樹中依賴data的Widget
@override
bool updateShouldNotify(ShareDataWidget old) {
//如果返回true,則子樹中依賴(build函數(shù)中有調(diào)用)本widget
//的子widget的`state.didChangeDependencies`會被調(diào)用
return old.data != data;
}
}
然后我們實現(xiàn)一個子組件_TestWidget
,在其build
方法中引用ShareDataWidget
中的數(shù)據(jù)。同時,在其didChangeDependencies()
回調(diào)中打印日志:
class _TestWidget extends StatefulWidget {
@override
__TestWidgetState createState() => new __TestWidgetState();
}
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
//使用InheritedWidget中的共享數(shù)據(jù)
return Text(ShareDataWidget
.of(context)
.data
.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//父或祖先widget中的InheritedWidget改變(updateShouldNotify返回true)時會被調(diào)用。
//如果build中沒有依賴InheritedWidget,則此回調(diào)不會被調(diào)用。
print("Dependencies change");
}
}
最后,我們創(chuàng)建一個按鈕,每點擊一次,就將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>[
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: _TestWidget(),//子widget中依賴ShareDataWidget
),
RaisedButton(
child: Text("Increment"),
//每點擊一次,將count自增,然后重新build,ShareDataWidget的data將被更新
onPressed: () => setState(() => ++count),
)
],
),
),
);
}
}
運行后界面如圖7-1所示:
每點擊一次按鈕,計數(shù)器就會自增,控制臺就會打印一句日志:
I/flutter ( 8513): Dependencies change
可見依賴發(fā)生變化后,其didChangeDependencies()
會被調(diào)用。但是讀者要注意,如果_TestWidget的build 方法中沒有使用 ShareDataWidget 的數(shù)據(jù),那么它的didChangeDependencies()
將不會被調(diào)用,因為它并沒有依賴 ShareDataWidget。例如,我們將__TestWidgetState
代碼改為下面這樣,didChangeDependencies()
將不會被調(diào)用:
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
// 使用InheritedWidget中的共享數(shù)據(jù)
// return Text(ShareDataWidget
// .of(context)
// .data
// .toString());
return Text("text");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// build方法中沒有依賴InheritedWidget,此回調(diào)不會被調(diào)用。
print("Dependencies change");
}
}
上面的代碼中,我們將build()
方法中依賴ShareDataWidget
的代碼注釋掉了,然后返回一個固定Text
,這樣一來,當點擊 Increment 按鈕后,ShareDataWidget
的data
雖然發(fā)生變化,但由于__TestWidgetState
并未依賴ShareDataWidget
,所以__TestWidgetState
的didChangeDependencies
方法不會被調(diào)用。其實,這個機制很好理解,因為在數(shù)據(jù)發(fā)生變化時只對使用該數(shù)據(jù)的 Widget 更新是合理并且性能友好的。
思考題:Flutter framework 是怎么知道子 widget 有沒有依賴 InheritedWidget 的?
一般來說,子 widget 很少會重寫此方法,因為在依賴改變后 framework 也都會調(diào)用build()
方法。但是,如果你需要在依賴改變后執(zhí)行一些昂貴的操作,比如網(wǎng)絡請求,這時最好的方式就是在此方法中執(zhí)行,這樣可以避免每次build()
都執(zhí)行這些昂貴操作。
現(xiàn)在來思考一下,如果我們只想在__TestWidgetState
中引用ShareDataWidget
數(shù)據(jù),但卻不希望在ShareDataWidget
發(fā)生變化時調(diào)用__TestWidgetState
的didChangeDependencies()
方法應該怎么辦?其實答案很簡單,我們只需要將ShareDataWidget.of()
的實現(xiàn)改一下即可:
//定義一個便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)
static ShareDataWidget of(BuildContext context) {
//return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
唯一的改動就是獲取ShareDataWidget
對象的方式,把dependOnInheritedWidgetOfExactType()
方法換成了context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget
,那么他們到底有什么區(qū)別呢,我們看一下這兩個方法的源碼(實現(xiàn)代碼在Element
類中,Context
和Element
的關系我們將在后面專門介紹):
@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ 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;
}
我們可以看到,dependOnInheritedWidgetOfExactType()
比 getElementForInheritedWidgetOfExactType()
多調(diào)了dependOnInheritedElement
方法,dependOnInheritedElement
源碼如下:
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
可以看到dependOnInheritedElement
方法中主要是注冊了依賴關系!看到這里也就清晰了,調(diào)用dependOnInheritedWidgetOfExactType()
和 getElementForInheritedWidgetOfExactType()
的區(qū)別就是前者會注冊依賴關系,而后者不會,所以在調(diào)用dependOnInheritedWidgetOfExactType()
時,InheritedWidget
和依賴它的子孫組件關系便完成了注冊,之后當InheritedWidget
發(fā)生變化時,就會更新依賴它的子孫組件,也就是會調(diào)這些子孫組件的didChangeDependencies()
方法和build()
方法。而當調(diào)用的是 getElementForInheritedWidgetOfExactType()
時,由于沒有注冊依賴關系,所以之后當InheritedWidget
發(fā)生變化時,就不會更新相應的子孫 Widget。
注意,如果將上面示例中ShareDataWidget.of()
方法實現(xiàn)改成調(diào)用getElementForInheritedWidgetOfExactType()
,運行示例后,點擊"Increment"按鈕,會發(fā)現(xiàn)__TestWidgetState
的didChangeDependencies()
方法確實不會再被調(diào)用,但是其build()
仍然會被調(diào)用!造成這個的原因其實是,點擊"Increment"按鈕后,會調(diào)用_InheritedWidgetTestRouteState
的setState()
方法,此時會重新構建整個頁面,由于示例中,__TestWidget
并沒有任何緩存,所以它也都會被重新構建,所以也會調(diào)用build()
方法。
那么,現(xiàn)在就帶來了一個問題:實際上,我們只想更新子樹中依賴了ShareDataWidget
的組件,而現(xiàn)在只要調(diào)用_InheritedWidgetTestRouteState
的setState()
方法,所有子節(jié)點都會被重新build,這很沒必要,那么有什么辦法可以避免呢?答案是緩存!一個簡單的做法就是通過封裝一個StatefulWidget
,將子 Widget 樹緩存起來,具體做法下一節(jié)我們將通過實現(xiàn)一個Provider
Widget 來演示如何緩存,以及如何利用InheritedWidget
來實現(xiàn) Flutter 全局狀態(tài)共享。
更多建議: