本篇將帶你深刻理解 Flutter 中 State 的工做機制,並經過對狀態管理框架 Provider 解析加深理解,看完這一篇你將更輕鬆的理解你的 「State 大後宮」 。前端
前文:git
⚠️第十二篇中更多講解狀態的是管理框架,本篇更多講解 Flutter 自己的狀態設計。 github
咱們知道 Flutter 宇宙中萬物皆 Widget
,而 Widget
是 @immutable
即不可變的,因此每一個 Widget
狀態都表明了一幀。面試
在這個基礎上, StatefulWidget
的 State
幫咱們實現了在 Widget
的跨幀繪製 ,也就是在每次 Widget
重繪的時候,經過 State
從新賦予 Widget
須要的繪製信息。redux
這就涉及 Flutter 中 Widget
的實現原理,在以前的篇章咱們介紹過,這裏咱們說兩個涉及的概念:bash
Flutter 中的 Widget
在通常狀況下,是須要經過 Element
轉化爲 RenderObject
去實現繪製的。app
Element
是 BuildContext
的實現類,同時 Element
持有 RenderObject
和 Widget
,咱們代碼中的 Widget build(BuildContext context) {}
方法,就是被 Element
調用的。框架
瞭解這個兩個概念後,咱們先看下圖,在 Flutter 中構建一個 Widget
,首先會建立出這個 Widget
的 Element
,而事實上 State
實現跨幀共享,就是將 State
保存在Element
中,這樣 Element
每次調用 Widget build()
時,是經過 state.build(this);
獲得的新 Widget
,因此寫在 State
的數據就得以複用了。less
那 State
是在哪裏被建立的?ide
以下圖所示,StatefulWidget
的 createState
是在 StatefulElement
的構建方法裏建立的, 這就保證了只要 Element
不被從新建立,State
就一直被複用。
同時咱們看 update
方法,當新的 StatefulWidget
被建立用於更新 UI 時,新的 widget
就會被從新賦予到 _state
中,而這的設定也致使一個常被新人忽略的問題。
咱們先看問題代碼,以下圖所示:
_DemoAppState
中,咱們建立了 DemoPage
, 而且把 data
變量賦給了它。DemoPage
在建立 createState
時,又將 data
經過直接傳入 _DemoPageState
。_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
天然就被更新了。
咱們常說的 setState
,實際上是調用了 markNeedsBuild
,markNeedsBuild
內部會標記 element
爲 diry
,而後在下一幀 WidgetsBinding.drawFrame
纔會被繪製,這能夠也看出 setState
並非當即生效的。
前面咱們聊了 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
就表明着這個 Widget
的 Element
, 在 InheritedElement
裏被「登記」到 _dependents
了。
而當 InheritedWidget
被更新時,以下代碼所示,_dependents
中的 Element
會被逐個執行 notifyDependent
,最後觸發 markNeedsBuild
,這也是爲何當 InheritedWidget
被更新時,經過如 Theme.of(context).primaryColor
引用的地方,也會觸發更新的緣由。
下面開始實際分析 Provider 。
爲何會有 Provider ?
由於 Flutter 與 React 技術棧的類似性,因此在 Flutter 中涌現了諸如flutter_redux
、flutter_dva
、 flutter_mobx
、 fish_flutter
等前端式的狀態管理,它們大多比較複雜,並且須要對框架概念有必定理解。
而做爲 Flutter 官方推薦的狀態管理 scoped_model
,又由於其設計較爲簡單,有些時候不適用於複雜的場景。
因此在經歷了一端坎坷以後,今年 Google I/O 大會以後, Provider 成了 Flutter 官方新推薦的狀態管理方式之一。
它的特色就是: 不復雜,好理解,代碼量不大的狀況下,能夠方便組合和控制刷新顆粒度 , 而原 Google 官方倉庫的狀態管理 flutter-provide 已宣告GG , provider 成了它的替代品。
⚠️注意,`provider` 比 `flutter-provide` 多了個 `r`。
題外話:之前面試時,偶爾會被面試官問到「你的開源項目代碼量也很少啊」這樣的問題,每次我都會笑而不語,雖然代碼量能表明一些成果,可是我是十分反對用代碼量來衡量貢獻價值,這和你用加班時長來衡量員工價值有什麼區別?
以下代碼所示, 實現的是一個點擊計數器,其中:
_ProviderPageState
中使用MultiProvider
提供了多個 providers
的支持。CountWidget
中經過 Consumer
獲取的 counter
,同時更新 _ProviderPageState
中的 AppBar
和 CountWidget
中的 Text
顯示。class _ProviderPageState extends State<ProviderPage> {
@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<ProviderModel>(context);
return new Text("Provider ${counter.count.toString()}");
},
)
),
body: CountWidget(),
),
);
}
}
class CountWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ProviderModel>(builder: (context, counter, _) {
return new Column(
children: <Widget>[
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();
觸發更新。
這裏幾個知識點是:
一、 Provider 的內部 DelegateWidget
是一個 StatefulWidget
,因此能夠更新且具備生命週期。
二、狀態共享是使用了 InheritedProvider
這個 InheritedWidget
實現的。
三、巧妙利用 MultiProvider
和 Consumer
封裝,實現了組合與刷新顆粒度控制。
接着咱們逐個分析
既然是狀態管理,那麼確定有 StatefulWidget
和 setState
調用。
在 Provider 中,一系列關於 StatefulWidget
的生命週期管理和更新,都是經過各類代理完成的,以下圖所示,上面代碼中咱們用到的 ChangeNotifierProvider
大體經歷了這樣的流程:
ChangeNotifierProvider
的 ChangeNotifer
會被執行 addListener
添加監聽 listener
。listener
內會調用 StateDelegate
的 StateSetter
方法,從而調用到 StatefulWidget
的 setState
。ChangeNotifer
的 notifyListeners
時,就會最終觸發 setState
更新。而咱們使用過的 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
的嵌套使用。
狀態共享確定須要 InheritedWidget
,InheritedProvider
就是InheritedWidget
的子類,全部的 Provider
實現都在 build
方法中使用 InheritedProvider
進行嵌套,實現 value
的共享。
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<T>(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<A>(context),
Provider.of<B>(context),
Provider.of<C>(context),
Provider.of<D>(context),
Provider.of<E>(context),
Provider.of<F>(context),
child,
);
複製代碼
這樣的設定,相信用過 BLoC 模式的同窗會感受很貼心,之前正經常使用作 BLoC 時,每一個 StreamBuilder
的 snapShot
只支持一種類型,多個時要不就是多個狀態合併到一個實體,要不就須要多個StreamBuilder嵌套。
固然,若是你想直接利用 LayoutBuilder
搭配 Provider.of<T>(context)
也是能夠的:
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
var counter = Provider.of<ProviderModel>(context);
return new Text("Provider ${counter.count.toString()}");
}
複製代碼
其餘的還有 ValueListenableProvider
、FutureProvider
、StreamProvider
等多種 Provider
,可見整個 Provider 的設計上更貼近 Flutter 的原生特性,同時設計也更好理解,而且兼顧了性能等問題。
Provider 的使用指南上,更詳細的 Vadaski 的 《Flutter | 狀態管理指南篇——Provider》 已經寫過,我就不重複寫輪子了,感興趣的能夠過去看看。
自此,第十五篇終於結束了!(///▽///)