Flutter實(shí)戰(zhàn) 數(shù)據(jù)共享(InheritedWidget)

2021-03-08 11:38 更新

InheritedWidget是 Flutter 中非常重要的一個(gè)功能型組件,它提供了一種數(shù)據(jù)在 widget 樹中從上到下傳遞、共享的方式,比如我們?cè)趹?yīng)用的根 widget 中通過InheritedWidget共享了一個(gè)數(shù)據(jù),那么我們便可以在任意子 widget 中來獲取該共享的數(shù)據(jù)!這個(gè)特性在一些需要在 widget 樹中共享數(shù)據(jù)的場(chǎng)景中非常方便!如 Flutter SDK 中正是通過 InheritedWidget 來共享應(yīng)用主題(Theme)和 Locale (當(dāng)前語(yǔ)言環(huán)境)信息的。

InheritedWidget和 React 中的 context 功能類似,和逐級(jí)傳遞數(shù)據(jù)相比,它們能實(shí)現(xiàn)組件跨級(jí)傳遞數(shù)據(jù)。InheritedWidget的在 widget 樹中數(shù)據(jù)傳遞方向是從上到下的,這和通知Notification(將在下一章中介紹)的傳遞方向正好相反。

#didChangeDependencies

在之前介紹StatefulWidget時(shí),我們提到State對(duì)象有一個(gè)didChangeDependencies回調(diào),它會(huì)在“依賴”發(fā)生變化時(shí)被 Flutter Framework 調(diào)用。而這個(gè)“依賴”指的就是子 widget 是否使用了父 widget 中InheritedWidget的數(shù)據(jù)!如果使用了,則代表子 widget 依賴有依賴InheritedWidget;如果沒有使用則代表沒有依賴。這種機(jī)制可以使子組件在所依賴的InheritedWidget變化時(shí)來更新自身!比如當(dāng)主題、locale(語(yǔ)言)等發(fā)生變化時(shí),依賴其的子 widget 的didChangeDependencies方法將會(huì)被調(diào)用。

下面我們看一下之前“計(jì)數(shù)器”示例應(yīng)用程序的InheritedWidget版本。需要說明的是,本示例主要是為了演示InheritedWidget的功能特性,并不是計(jì)數(shù)器的推薦實(shí)現(xiàn)方式。

首先,我們通過繼承InheritedWidget,將當(dāng)前計(jì)數(shù)器點(diǎn)擊次數(shù)保存在ShareDataWidgetdata屬性中:

  1. class ShareDataWidget extends InheritedWidget {
  2. ShareDataWidget({
  3. @required this.data,
  4. Widget child
  5. }) :super(child: child);
  6. final int data; //需要在子樹中共享的數(shù)據(jù),保存點(diǎn)擊次數(shù)
  7. //定義一個(gè)便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)
  8. static ShareDataWidget of(BuildContext context) {
  9. return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  10. }
  11. //該回調(diào)決定當(dāng)data發(fā)生變化時(shí),是否通知子樹中依賴data的Widget
  12. @override
  13. bool updateShouldNotify(ShareDataWidget old) {
  14. //如果返回true,則子樹中依賴(build函數(shù)中有調(diào)用)本widget
  15. //的子widget的`state.didChangeDependencies`會(huì)被調(diào)用
  16. return old.data != data;
  17. }
  18. }

然后我們實(shí)現(xiàn)一個(gè)子組件_TestWidget,在其build方法中引用ShareDataWidget中的數(shù)據(jù)。同時(shí),在其didChangeDependencies() 回調(diào)中打印日志:

  1. class _TestWidget extends StatefulWidget {
  2. @override
  3. __TestWidgetState createState() => new __TestWidgetState();
  4. }
  5. class __TestWidgetState extends State<_TestWidget> {
  6. @override
  7. Widget build(BuildContext context) {
  8. //使用InheritedWidget中的共享數(shù)據(jù)
  9. return Text(ShareDataWidget
  10. .of(context)
  11. .data
  12. .toString());
  13. }
  14. @override
  15. void didChangeDependencies() {
  16. super.didChangeDependencies();
  17. //父或祖先widget中的InheritedWidget改變(updateShouldNotify返回true)時(shí)會(huì)被調(diào)用。
  18. //如果build中沒有依賴InheritedWidget,則此回調(diào)不會(huì)被調(diào)用。
  19. print("Dependencies change");
  20. }
  21. }

最后,我們創(chuàng)建一個(gè)按鈕,每點(diǎn)擊一次,就將ShareDataWidget的值自增:

  1. class InheritedWidgetTestRoute extends StatefulWidget {
  2. @override
  3. _InheritedWidgetTestRouteState createState() => new _InheritedWidgetTestRouteState();
  4. }
  5. class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
  6. int count = 0;
  7. @override
  8. Widget build(BuildContext context) {
  9. return Center(
  10. child: ShareDataWidget( //使用ShareDataWidget
  11. data: count,
  12. child: Column(
  13. mainAxisAlignment: MainAxisAlignment.center,
  14. children: <Widget>[
  15. Padding(
  16. padding: const EdgeInsets.only(bottom: 20.0),
  17. child: _TestWidget(),//子widget中依賴ShareDataWidget
  18. ),
  19. RaisedButton(
  20. child: Text("Increment"),
  21. //每點(diǎn)擊一次,將count自增,然后重新build,ShareDataWidget的data將被更新
  22. onPressed: () => setState(() => ++count),
  23. )
  24. ],
  25. ),
  26. ),
  27. );
  28. }
  29. }

運(yùn)行后界面如圖7-1所示:

圖7-1

每點(diǎn)擊一次按鈕,計(jì)數(shù)器就會(huì)自增,控制臺(tái)就會(huì)打印一句日志:

  1. I/flutter ( 8513): Dependencies change

可見依賴發(fā)生變化后,其didChangeDependencies()會(huì)被調(diào)用。但是讀者要注意,如果_TestWidget的build 方法中沒有使用 ShareDataWidget 的數(shù)據(jù),那么它的didChangeDependencies()將不會(huì)被調(diào)用,因?yàn)樗]有依賴 ShareDataWidget。例如,我們將__TestWidgetState代碼改為下面這樣,didChangeDependencies()將不會(huì)被調(diào)用:

  1. class __TestWidgetState extends State<_TestWidget> {
  2. @override
  3. Widget build(BuildContext context) {
  4. // 使用InheritedWidget中的共享數(shù)據(jù)
  5. // return Text(ShareDataWidget
  6. // .of(context)
  7. // .data
  8. // .toString());
  9. return Text("text");
  10. }
  11. @override
  12. void didChangeDependencies() {
  13. super.didChangeDependencies();
  14. // build方法中沒有依賴InheritedWidget,此回調(diào)不會(huì)被調(diào)用。
  15. print("Dependencies change");
  16. }
  17. }

上面的代碼中,我們將build()方法中依賴ShareDataWidget的代碼注釋掉了,然后返回一個(gè)固定Text,這樣一來,當(dāng)點(diǎn)擊 Increment 按鈕后,ShareDataWidgetdata雖然發(fā)生變化,但由于__TestWidgetState并未依賴ShareDataWidget,所以__TestWidgetStatedidChangeDependencies方法不會(huì)被調(diào)用。其實(shí),這個(gè)機(jī)制很好理解,因?yàn)樵跀?shù)據(jù)發(fā)生變化時(shí)只對(duì)使用該數(shù)據(jù)的 Widget 更新是合理并且性能友好的。

思考題:Flutter framework 是怎么知道子 widget 有沒有依賴 InheritedWidget 的?

#應(yīng)該在didChangeDependencies()中做什么?

一般來說,子 widget 很少會(huì)重寫此方法,因?yàn)樵谝蕾嚫淖兒?framework 也都會(huì)調(diào)用build()方法。但是,如果你需要在依賴改變后執(zhí)行一些昂貴的操作,比如網(wǎng)絡(luò)請(qǐng)求,這時(shí)最好的方式就是在此方法中執(zhí)行,這樣可以避免每次build()都執(zhí)行這些昂貴操作。

#深入了解InheritedWidget

現(xiàn)在來思考一下,如果我們只想在__TestWidgetState中引用ShareDataWidget數(shù)據(jù),但卻不希望在ShareDataWidget發(fā)生變化時(shí)調(diào)用__TestWidgetStatedidChangeDependencies()方法應(yīng)該怎么辦?其實(shí)答案很簡(jiǎn)單,我們只需要將ShareDataWidget.of()的實(shí)現(xiàn)改一下即可:

  1. //定義一個(gè)便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)
  2. static ShareDataWidget of(BuildContext context) {
  3. //return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  4. return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
  5. }

唯一的改動(dòng)就是獲取ShareDataWidget對(duì)象的方式,把dependOnInheritedWidgetOfExactType()方法換成了context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget,那么他們到底有什么區(qū)別呢,我們看一下這兩個(gè)方法的源碼(實(shí)現(xiàn)代碼在Element類中,ContextElement的關(guān)系我們將在后面專門介紹):

  1. @override
  2. InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  3. assert(_debugCheckStateIsActiveForAncestorLookup());
  4. final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  5. return ancestor;
  6. }
  7. @override
  8. InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
  9. assert(_debugCheckStateIsActiveForAncestorLookup());
  10. final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  11. //多出的部分
  12. if (ancestor != null) {
  13. assert(ancestor is InheritedElement);
  14. return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  15. }
  16. _hadUnsatisfiedDependencies = true;
  17. return null;
  18. }

我們可以看到,dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()多調(diào)了dependOnInheritedElement方法,dependOnInheritedElement源碼如下:

  1. @override
  2. InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
  3. assert(ancestor != null);
  4. _dependencies ??= HashSet<InheritedElement>();
  5. _dependencies.add(ancestor);
  6. ancestor.updateDependencies(this, aspect);
  7. return ancestor.widget;
  8. }

可以看到dependOnInheritedElement方法中主要是注冊(cè)了依賴關(guān)系!看到這里也就清晰了,調(diào)用dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()的區(qū)別就是前者會(huì)注冊(cè)依賴關(guān)系,而后者不會(huì),所以在調(diào)用dependOnInheritedWidgetOfExactType()時(shí),InheritedWidget和依賴它的子孫組件關(guān)系便完成了注冊(cè),之后當(dāng)InheritedWidget發(fā)生變化時(shí),就會(huì)更新依賴它的子孫組件,也就是會(huì)調(diào)這些子孫組件的didChangeDependencies()方法和build()方法。而當(dāng)調(diào)用的是 getElementForInheritedWidgetOfExactType()時(shí),由于沒有注冊(cè)依賴關(guān)系,所以之后當(dāng)InheritedWidget發(fā)生變化時(shí),就不會(huì)更新相應(yīng)的子孫 Widget。

注意,如果將上面示例中ShareDataWidget.of()方法實(shí)現(xiàn)改成調(diào)用getElementForInheritedWidgetOfExactType(),運(yùn)行示例后,點(diǎn)擊"Increment"按鈕,會(huì)發(fā)現(xiàn)__TestWidgetStatedidChangeDependencies()方法確實(shí)不會(huì)再被調(diào)用,但是其build()仍然會(huì)被調(diào)用!造成這個(gè)的原因其實(shí)是,點(diǎn)擊"Increment"按鈕后,會(huì)調(diào)用_InheritedWidgetTestRouteStatesetState()方法,此時(shí)會(huì)重新構(gòu)建整個(gè)頁(yè)面,由于示例中,__TestWidget 并沒有任何緩存,所以它也都會(huì)被重新構(gòu)建,所以也會(huì)調(diào)用build()方法。

那么,現(xiàn)在就帶來了一個(gè)問題:實(shí)際上,我們只想更新子樹中依賴了ShareDataWidget的組件,而現(xiàn)在只要調(diào)用_InheritedWidgetTestRouteStatesetState()方法,所有子節(jié)點(diǎn)都會(huì)被重新build,這很沒必要,那么有什么辦法可以避免呢?答案是緩存!一個(gè)簡(jiǎn)單的做法就是通過封裝一個(gè)StatefulWidget,將子 Widget 樹緩存起來,具體做法下一節(jié)我們將通過實(shí)現(xiàn)一個(gè)Provider Widget 來演示如何緩存,以及如何利用InheritedWidget 來實(shí)現(xiàn) Flutter 全局狀態(tài)共享。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)