本篇將帶你深入理解 Flutter 中 State 的工作機制,並通過對狀態管理框架 Provider 解析加深理解,看完這一篇你將更輕鬆的理解你的 “State 大後宮” 。
前文:
⚠️第十二篇中更多講解狀態的是管理框架,本篇更多講解 Flutter 本身的狀態設計。
一、State
1、State 是什麼?
我們知道 Flutter 宇宙中萬物皆 Widget
,而 Widget
是 @immutable
即不可變的,所以每個 Widget
狀態都代表了一幀。
在這個基礎上, StatefulWidget
的 State
幫我們實現了在 Widget
的跨幀繪製 ,也就是在每次 Widget
重繪的時候,通過 State
重新賦予 Widget
需要的繪製資訊。
2、State 怎麼實現跨幀共享?
這就涉及 Flutter 中 Widget
的實現原理,在之前的篇章我們介紹過,這裡我們說兩個涉及的概念:
-
Flutter 中的
Widget
在一般情況下,是需要通過Element
轉化為RenderObject
去實現繪製的。 -
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
的資料就得以複用了。
那 State
是在哪裡被建立的?
如下圖所示,StatefulWidget
的 createState
是在 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
,其實是呼叫了 markNeedsBuild
,markNeedsBuild
內部會標記 element
為 diry
,然後在下一幀 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
就代表著這個 Widget
的 Element
, 在 InheritedElement
裡被“登記”到 _dependents
了。
而當 InheritedWidget
被更新時,如下程式碼所示,_dependents
中的 Element
會被逐個執行 notifyDependent
,最後觸發 markNeedsBuild
,這也是為什麼當 InheritedWidget
被更新時,通過如 Theme.of(context).primaryColor
引用的地方,也會觸發更新的原因。
下面開始實際分析 Provider 。
二、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`。
題外話:以前面試時,偶爾會被面試官問到“你的開源專案程式碼量也不多啊”這樣的問題,每次我都會笑而不語,雖然程式碼量能代表一些成果,但是我是十分反對用程式碼量來衡量貢獻價值,這和你用加班時長來衡量員工價值有什麼區別?
0、演示程式碼
如下程式碼所示, 實現的是一個點選計數器,其中:
_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();
觸發更新。
這裡幾個知識點是:
-
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
更新。
而我們使用過的 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<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》 已經寫過,我就不重複寫輪子了,感興趣的可以過去看看。
自此,第十五篇終於結束了!(///▽///)
資源推薦
- Github : github.com/CarGuo
- 本文Demo :github.com/CarGuo/stat…
- 完整專案 :github.com/CarGuo/GSYG…