本篇將帶你深入理解 Flutter 中 State 的工作機制,並通過對狀態管理框架 Provider 解析加深理解,看完這一篇你將更輕鬆的理解你的 「State 大後宮」 。

前文:

一、 Dart語言和Flutter基礎 二、 快速開發實戰篇 三、 打包與填坑篇

四、 Redux、主題、國際化

五、 深入探索 六、 深入Widget原理 七、 深入布局原理 八、 實用技巧與填坑 九、 深入繪製原理 十、 深入圖片載入流程 十一、全面深入理解Stream 十二、全面深入理解狀態管理設計 十三、全面深入觸摸和滑動原理

十四、混合開發打包 Android 篇

??第十二篇中更多講解狀態的是管理框架,本篇更多講解 Flutter 本身的狀態設計。

一、State

1、State 是什麼?

我們知道 Flutter 宇宙中萬物皆 Widget ,而 Widget@immutable 即不可變的,所以每個 Widget 狀態都代表了一幀。

在這個基礎上, StatefulWidgetState 幫我們實現了在 Widget 的跨幀繪製 ,也就是在每次 Widget 重繪的時候,通過 State 重新賦予 Widget 需要的繪製信息。

2、State 怎麼實現跨幀共享?

這就涉及 Flutter 中 Widget 的實現原理,在之前的篇章我們介紹過,這裡我們說兩個涉及的概念:

  • Flutter 中的 Widget 在一般情況下,是需要通過 Element 轉化為 RenderObject 去實現繪製的。
  • ElementBuildContext 的實現類,同時 Element 持有 RenderObjectWidget我們代碼中的 Widget build(BuildContext context) {} 方法,就是被 Element 調用的。

了解這個兩個概念後,我們先看下圖,在 Flutter 中構建一個 Widget ,首先會創建出這個 WidgetElement而事實上 State 實現跨幀共享,就是將 State 保存在Element 中,這樣 Element 每次調用 Widget build() 時,是通過 state.build(this); 得到的新 Widget ,所以寫在 State 的數據就得以復用了。

State 是在哪裡被創建的?

如下圖所示,StatefulWidgetcreateState 是在 StatefulElement 的構建方法里創建的, 這就保證了只要 Element 不被重新創建,State 就一直被複用。

同時我們看 update 方法,當新的 StatefulWidget 被創建用於更新 UI 時,新的 widget 就會被重新賦予到 _state 中,而這的設定也導致一個常被新人忽略的問題。

我們先看問題代碼,如下圖所示:

  • 1、在 _DemoAppState 中,我們創建了 DemoPage , 並且把 data 變數賦給了它。
  • 2、DemoPage 在創建 createState 時,又將 data 通過直接傳入 _DemoPageState
  • 3、在 _DemoPageState 中直接將傳入的 data 通過 Text 顯示出來。

運行後我們一看也沒什麼問題吧? 但是當我們點擊 4 中的 setState 時,卻發現 3 中 Text 沒有發現改變, 這是為什麼呢?

問題就在於前面 StatefulElement 的構建方法和 update 方法:

State 只在 StatefulElement 的構建方法中創建,當我們調用 setState 觸發 update 時,只是執行了 _state.widget = newWidget,而我們通過 _DemoPageState(this.data) 傳入的 data ,在傳入後執行setState 時並沒有改變。

如果我們採用上圖代碼中 3 注釋的 widget.data 方法,因為 _state.widget = newWidget 時,State 中的 Widget 已經被更新了,Text 自然就被更新了。

3、setState 幹了什麼?

我們常說的 setState ,其實是調用了 markNeedsBuildmarkNeedsBuild 內部會標記 elementdiry,然後在下一幀 WidgetsBinding.drawFrame 才會被繪製,這可以也看出 setState 並不是立即生效的。

4、狀態共享

前面我們聊了 Flutter 中 State 的作用和工作原理,接下來我們看一個老生常談的對象: InheritedWidget

狀態共享是常見的需求,比如用戶信息和登陸狀態等等,而 Flutter 中 InheritedWidget 就是為此而設計的,在第十二篇我們大致講過它:

Element 的內部有一個 Map<Type, InheritedElement> _inheritedWidgets; 參數,_inheritedWidgets 一般情況下是空的,只有當父控制項是 InheritedWidget 或者本身是 InheritedWidgets 時,它才會有被初始化,而當父控制項是 InheritedWidget 時,這個 Map 會被一級一級往下傳遞與合併。

所以當我們通過 context 調用 inheritFromWidgetOfExactType 時,就可以通過這個 Map 往上查找,從而找到這個上級的 InheritedWidget

噢,是的,InheritedWidget 共享的是 Widget ,只是這個 Widget 是一個 ProxyWidget ,它自己本身並不繪製什麼,但共享這個 Widget 內保存有的值,卻達到了共享狀態的目的。

如下代碼所示,Flutter 內 Theme 的共享,共享的其實是 _InheritedTheme 這個 Widget ,而我們通過 Theme.of(context) 拿到的,其實就是保存在這個 Widget 內的 ThemeData

static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
final _InheritedTheme inheritedTheme = context.inheritFromWidgetOfExactType(_InheritedTheme);
if (shadowThemeOnly) {
/// inheritedTheme 這個 Widget 內的 theme
/// theme 內有我們需要的 ThemeData
return inheritedTheme.theme.data;
}
···
}

這裡有個需要注意的點,就是 inheritFromWidgetOfExactType 方法剛了什麼?

我們直接找到 Element 中的 inheritFromWidgetOfExactType 方法實現,如下關鍵代碼所示:

  • 首先從 _inheritedWidgets 中查找是否有該類型的 InheritedElement
  • 查找到後添加到 _dependencies 中,並且通過 updateDependencies 將當前 Element 添加到 InheritedElement_dependents 這個Map 里。
  • 返回 InheritedElement 中的 Widget

@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
/// 在共享 map _inheritedWidgets 中查找
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
/// 返回找到的 InheritedWidget ,同時添加當前 element 處理
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}

@override
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
/// 就是將當前 element(this) 添加到 _dependents 里
/// 也就是 InheritedElement 的 _dependents
/// _dependents[dependent] = value;
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}

@override
void notifyClients(InheritedWidget oldWidget) {
for (Element dependent in _dependents.keys) {
notifyDependent(oldWidget, dependent);
}
}

這裡面的關鍵就是 ancestor.updateDependencies(this, aspect); 這個方法:

我們都知道,獲取 InheritedWidget 一般需要 BuildContext ,如Theme.of(context) ,而 BuildContext 的實現就是 Element所以當我們調用 context.inheritFromWidgetOfExactType 時,就會將這個 context 所代表的 Element 添加到 InheritedElement_dependents 中。

這代表著什麼?

比如當我們在 StatefulWidget 中調用 Theme.of(context).primaryColor 時,傳入的 context 就代表著這個 WidgetElement, 在 InheritedElement 里被「登記」到 _dependents 了。

而當 InheritedWidget 被更新時,如下代碼所示,_dependents 中的 Element 會被逐個執行 notifyDependent ,最後觸發 markNeedsBuild ,這也是為什麼當 InheritedWidget 被更新時,通過如 Theme.of(context).primaryColor 引用的地方,也會觸發更新的原因。

下面開始實際分析 Provider

二、Provider

為什麼會有 Provider

因為 Flutter 與 React 技術棧的相似性,所以在 Flutter 中湧現了諸如flutter_reduxflutter_dvaflutter_mobxfish_flutter 等前端式的狀態管理,它們大多比較複雜,而且需要對框架概念有一定理解。

而作為 Flutter 官方推薦的狀態管理 scoped_model ,又因為其設計較為簡單,有些時候不適用於複雜的場景。

所以在經歷了一端坎坷之後,今年 Google I/O 大會之後, Provider 成了 Flutter 官方新推薦的狀態管理方式之一。

它的特點就是: 不複雜,好理解,代碼量不大的情況下,可以方便組合和控制刷新顆粒度 , 而原 Google 官方倉庫的狀態管理 flutter-provide 已宣告GG , provider 成了它的替代品。

``! ??注意,providerflutter-provide多了個r`。

> 題外話:以前面試時,偶爾會被面試官問到「你的開源項目代碼量也不多啊」這樣的問題,每次我都會笑而不語,**雖然代碼量能代表一些成果,但是我是十分反對用代碼量來衡量貢獻價值,這和你用加班時長來衡量員工價值有什麼區別?**

### 0、演示代碼

如下代碼所示, 實現的是一個點擊計數器,其中:

- `_ProviderPageState` 中使用`MultiProvider` 提供了多個 `providers` 的支持。
- 在 `CountWidget` 中通過 `Consumer` 獲取的 `counter ` ,同時更新 `_ProviderPageState` 中的 `AppBar` 和 `CountWidget ` 中的 `Text ` 顯示。

class ProviderPageState extends State { @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(builder: () => ProviderModel()), ], child: Scaffold( appBar: AppBar( title: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var counter = Provider.of(context); return new Text("Provider ${counter.count.toString()}"); }, ) ), body: CountWidget(), ), ); } }

class CountWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer(builder: (context, counter, _) { return new Column( children: [ new Expanded(child: new Center(child: new Text(counter.count.toString()))), new Center( child: new FlatButton( onPressed: () { counter.add(); }, color: Colors.blue, child: new Text("+")), ) ], ); }); } }

class ProviderModel extends ChangeNotifier { int _count = 0;

int get count => _count;

void add() { _count++; notifyListeners(); } }

所以上述代碼中,我們通過 `ChangeNotifierProvider ` 組合了 `ChangeNotifier` (ProviderModel) 實現共享;利用了 ` Provider.of` 和 `Consumer ` 獲取共享的 `counter` 狀態;通過調用 `ChangeNotifier` 的 ` notifyListeners();` 觸發更新。

這裡幾個知識點是:

- 1、 **Provider** 的內部 `DelegateWidget` 是一個 `StatefulWidget` ,所以可以更新且具有生命周期。

- 2、狀態共享是使用了 `InheritedProvider` 這個 `InheritedWidget` 實現的。

- 3、巧妙利用 `MultiProvider` 和 `Consumer` 封裝,實現了組合與刷新顆粒度控制。

接著我們逐個分析

### 1、Delegate

既然是狀態管理,那麼肯定有 `StatefulWidget` 和 `setState` 調用。

在 **Provider** 中,一系列關於 `StatefulWidget` 的生命周期管理和更新,都是通過各種代理完成的,如下圖所示,上面代碼中我們用到的 `ChangeNotifierProvider ` 大致經歷了這樣的流程:

- 設置到 `ChangeNotifierProvider ` 的 `ChangeNotifer` 會被執行 `addListener` 添加監聽 `listener`。
- `listener` 內會調用 `StateDelegate` 的 `StateSetter` 方法,從而調用到 `StatefulWidget` 的 `setState`。
- 當我們執行 `ChangeNotifer` 的 `notifyListeners ` 時,就會最終觸發 `setState` 更新。

![](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image6)

而我們使用過的 `MultiProvider` 則是允許我們組合多種 `Provider` ,如下代碼所示,傳入的 `providers` 會倒序排列,最後組合成一個嵌套的 Widget tree ,方便我們添加多種 `Provider` :

@override Widget build(BuildContext context) { var tree = child; for (final provider in providers.reversed) { tree = provider.cloneWithChild(tree); } return tree; }

/// Clones the current provider with a new [child]. /// Note for implementers: all other values, including [Key] must be /// preserved. @override MultiProvider cloneWithChild(Widget child) { return MultiProvider( key: key, providers: providers, child: child, ); }

通過 `Delegate` 中回調出來的各種生命周期,如 `Disposer `,也有利於我們外部二次處理,減少外部 `StatefulWidget ` 的嵌套使用。

### 2、InheritedProvider

狀態共享肯定需要 `InheritedWidget` ,`InheritedProvider ` 就是`InheritedWidget ` 的子類,所有的 `Provider` 實現都在 `build` 方法中使用 `InheritedProvider ` 進行嵌套,實現 `value` 的共享。

### 3、Consumer

`Consumer ` 是 `Provider` 中比較有意思的東西,它本身是一個 `StatelessWidget` , 只是在 `build ` 中通過 ` Provider.of<T>(context)` 幫你獲取到 `InheritedWidget` 共享的 `value` 。

final Widget Function(BuildContext context, T value, Widget child) builder;

@override Widget build(BuildContext context) { return builder( context, Provider.of(context), child, ); }

那我們直接使用 `Provider.of<T>(context)` ,不使用 `Consumer ` 可以嗎?

當然可以,但是你還記得前面,我們在介紹 `InheritedWidget` 時所說的:

> 傳入的 `context` 代表著這個 `Widget` 的 `Element` 在 `InheritedElement` 里被「登記」到 `_dependents` 了。

`Consumer ` 做為一個單獨 `StatelessWidget` ,**它的好處就是 `Provider.of<T>(context)` 傳入的 `context` 就是 `Consumer ` 它自己。** 這樣的話,我們在需要使用 `Provider.value` 的地方用 `Consumer` 做嵌套, `InheritedWidget` 更新的時候,就不會更新到整個頁面 , 而是僅更新到 `Consumer ` 這個 `StatelessWidget` 。

**所以 `Consumer ` 貼心的封裝了 `context` 在 `InheritedWidget` 中的「登記邏輯」,從而控制了狀態改變時,需要更新的精細度。**

同時庫內還提供了 `Consumer2` ~ `Consumer6` 的組合,感受下 :

@override Widget build(BuildContext context) { return builder( context, Provider.of(context), Provider.of(context), Provider.of(context), Provider.of(context), Provider.of(context), Provider.of(context), child, );

這樣的設定,相信用過 BLoC 模式的同學會感覺很貼心,以前正常用做 BLoC 時,每個 `StreamBuilder` 的 `snapShot` 只支持一種類型,多個時*要不就是多個狀態合併到一個實體,要不就需要多個StreamBuilder嵌套。*

當然,如果你想直接利用 `LayoutBuilder` 搭配 `Provider.of<T>(context)` 也是可以的:

LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var counter = Provider.of(context); return new Text("Provider ${counter.count.toString()}"); } ```

f="">其他的還有 ValueListenableProvider 、FutureProvider 、StreamProvider 等多種 Provider ,可見整個 Provider 的設計上更貼近 Flutter 的原生特性,同時設計也更好理解,並且兼顧了性能等問題。

Provider 的使用指南上,更詳細的 Vadaski 的 《Flutter | 狀態管理指南篇——Provider》 已經寫過,我就不重複寫輪子了,感興趣的可以過去看看。

自此,第十五篇終於結束了!(///▽///)

資源推薦

  • Github : github.com/CarGuo
  • 本文Demo :github.com/CarGuo/state
  • 完整項目 :github.com/CarGuo/GSYGi

完整開源項目推薦:

  • GSY Flutter 實戰系列電子書
  • GSYGithubApp Flutter
  • GSYGithubApp React Native
  • GSYGithubAppWeex


推薦閱讀:
相关文章