本篇將帶你深入理解 Flutter 中 State 的工作機制糯耍,并通過對狀態(tài)管理框架 Provider 解析加深理解翰苫,看完這一篇你將更輕松的理解你的 “State 大后宮” 。
文章匯總地址:
??第十二篇中更多講解狀態(tài)的是管理框架谨履,本篇更多講解 Flutter 本身的狀態(tài)設計欢摄。
一、State
1笋粟、State 是什么怀挠?
我們知道 Flutter 宇宙中萬物皆 Widget
析蝴,而 Widget
是 @immutable
即不可變的,所以每個 Widget
狀態(tài)都代表了一幀绿淋。
在這個基礎上嫌变, StatefulWidget
的 State
幫我們實現(xiàn)了在 Widget
的跨幀繪制 ,也就是在每次 Widget
重繪的時候躬它,通過 State
重新賦予 Widget
需要的繪制信息。
2东涡、State 怎么實現(xiàn)跨幀共享冯吓?
這就涉及 Flutter 中 Widget
的實現(xiàn)原理,在之前的篇章我們介紹過疮跑,這里我們說兩個涉及的概念:
Flutter 中的
Widget
在一般情況下组贺,是需要通過Element
轉化為RenderObject
去實現(xiàn)繪制的。Element
是BuildContext
的實現(xiàn)類祖娘,同時Element
持有RenderObject
和Widget
失尖,我們代碼中的Widget build(BuildContext context) {}
方法,就是被Element
調用的渐苏。
了解這個兩個概念后掀潮,我們先看下圖,在 Flutter 中構建一個 Widget
琼富,首先會創(chuàng)建出這個 Widget
的 Element
仪吧,而事實上 State
實現(xiàn)跨幀共享,就是將 State
保存在Element
中鞠眉,這樣 Element
每次調用 Widget build()
時薯鼠,是通過 state.build(this);
得到的新 Widget
,所以寫在 State
的數(shù)據(jù)就得以復用了械蹋。
那 State
是在哪里被創(chuàng)建的出皇?
如下圖所示,StatefulWidget
的 createState
是在 StatefulElement
的構建方法里創(chuàng)建的哗戈, 這就保證了只要 Element
不被重新創(chuàng)建郊艘,State
就一直被復用。
同時我們看 update
方法谱醇,當新的 StatefulWidget
被創(chuàng)建用于更新 UI 時暇仲,新的 widget
就會被重新賦予到 _state
中,而這的設定也導致一個常被新人忽略的問題副渴。
我們先看問題代碼奈附,如下圖所示:
- 1、在
_DemoAppState
中煮剧,我們創(chuàng)建了DemoPage
, 并且把data
變量賦給了它斥滤。 - 2将鸵、
DemoPage
在創(chuàng)建createState
時,又將data
通過直接傳入_DemoPageState
佑颇。 - 3顶掉、在
_DemoPageState
中直接將傳入的data
通過Text
顯示出來。
運行后我們一看也沒什么問題吧挑胸? 但是當我們點擊 4 中的 setState
時痒筒,卻發(fā)現(xiàn) 3 中 Text
沒有發(fā)現(xiàn)改變, 這是為什么呢茬贵?
問題就在于前面 StatefulElement
的構建方法和 update
方法:
State
只在 StatefulElement
的構建方法中創(chuàng)建簿透,當我們調用 setState
觸發(fā) update
時,只是執(zhí)行了 _state.widget = newWidget
解藻,而我們通過 _DemoPageState(this.data)
傳入的 data 老充,在傳入后執(zhí)行setState
時并沒有改變。
如果我們采用上圖代碼中 3 注釋的 widget.data
方法螟左,因為 _state.widget = newWidget
時啡浊,State
中的 Widget
已經被更新了,Text
自然就被更新了胶背。
3巷嚣、setState 干了什么?
我們常說的 setState
钳吟,其實是調用了 markNeedsBuild
涂籽,markNeedsBuild
內部會標記 element
為 diry
乐导,然后在下一幀 WidgetsBinding.drawFrame
才會被繪制予跌,這可以也看出 setState
并不是立即生效的讹蘑。
4堕绩、狀態(tài)共享
前面我們聊了 Flutter 中 State
的作用和工作原理腹忽,接下來我們看一個老生常談的對象: InheritedWidget
奋单。
狀態(tài)共享是常見的需求闻察,比如用戶信息和登陸狀態(tài)等等捆探,而 Flutter 中 InheritedWidget
就是為此而設計的奔誓,在第十二篇我們大致講過它:
在
Element
的內部有一個Map<Type, InheritedElement> _inheritedWidgets;
參數(shù)斤吐,_inheritedWidgets
一般情況下是空的,只有當父控件是InheritedWidget
或者本身是InheritedWidgets
時厨喂,它才會有被初始化和措,而當父控件是InheritedWidget
時,這個Map
會被一級一級往下傳遞與合并蜕煌。所以當我們通過
context
調用inheritFromWidgetOfExactType
時派阱,就可以通過這個Map
往上查找,從而找到這個上級的InheritedWidget
斜纪。
噢贫母,是的文兑,InheritedWidget
共享的是 Widget
,只是這個 Widget
是一個 ProxyWidget
腺劣,它自己本身并不繪制什么绿贞,但共享這個 Widget
內保存有的值,卻達到了共享狀態(tài)的目的橘原。
如下代碼所示籍铁,F(xiàn)lutter 內 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
方法實現(xiàn)耻警,如下關鍵代碼所示:
- 首先從
_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
的實現(xiàn)就是 Element
募判,所以當我們調用 context.inheritFromWidgetOfExactType
時荡含,就會將這個 context
所代表的 Element
添加到 InheritedElement
的 _dependents
中。
這代表著什么届垫?
比如當我們在 StatefulWidget
中調用 Theme.of(context).primaryColor
時释液,傳入的 context
就代表著這個 Widget
的 Element
, 在 InheritedElement
里被“登記”到 _dependents
了装处。
而當 InheritedWidget
被更新時误债,如下代碼所示,_dependents
中的 Element
會被逐個執(zhí)行 notifyDependent
妄迁,最后觸發(fā) markNeedsBuild
寝蹈,這也是為什么當 InheritedWidget
被更新時,通過如 Theme.of(context).primaryColor
引用的地方登淘,也會觸發(fā)更新的原因箫老。
下面開始實際分析 Provider 。
二黔州、Provider
為什么會有 Provider 槽惫?
因為 Flutter 與 React 技術棧的相似性周叮,所以在 Flutter 中涌現(xiàn)了諸如flutter_redux
、flutter_dva
界斜、 flutter_mobx
仿耽、 fish_flutter
等前端式的狀態(tài)管理,它們大多比較復雜各薇,而且需要對框架概念有一定理解项贺。
而作為 Flutter 官方推薦的狀態(tài)管理 scoped_model
,又因為其設計較為簡單峭判,有些時候不適用于復雜的場景开缎。
所以在經歷了一端坎坷之后,今年 Google I/O 大會之后林螃, Provider 成了 Flutter 官方新推薦的狀態(tài)管理方式之一奕删。
它的特點就是: 不復雜,好理解疗认,代碼量不大的情況下完残,可以方便組合和控制刷新顆粒度 , 而原 Google 官方倉庫的狀態(tài)管理 flutter-provide 已宣告GG 横漏, provider 成了它的替代品谨设。
??注意,`provider` 比 `flutter-provide` 多了個 `r`缎浇。
題外話:以前面試時扎拣,偶爾會被面試官問到“你的開源項目代碼量也不多啊”這樣的問題,每次我都會笑而不語素跺,雖然代碼量能代表一些成果二蓝,但是我是十分反對用代碼量來衡量貢獻價值,這和你用加班時長來衡量員工價值有什么區(qū)別指厌?
0侣夷、演示代碼
如下代碼所示, 實現(xiàn)的是一個點擊計數(shù)器仑乌,其中:
-
_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) 實現(xiàn)共享;利用了 Provider.of
和 Consumer
獲取共享的 counter
狀態(tài)厕九;通過調用 ChangeNotifier
的 notifyListeners();
觸發(fā)更新蓖捶。
這里幾個知識點是:
1、 Provider 的內部
DelegateWidget
是一個StatefulWidget
扁远,所以可以更新且具有生命周期俊鱼。2刻像、狀態(tài)共享是使用了
InheritedProvider
這個InheritedWidget
實現(xiàn)的。3并闲、巧妙利用
MultiProvider
和Consumer
封裝细睡,實現(xiàn)了組合與刷新顆粒度控制。
接著我們逐個分析
1帝火、Delegate
既然是狀態(tài)管理溜徙,那么肯定有 StatefulWidget
和 setState
調用。
在 Provider 中犀填,一系列關于 StatefulWidget
的生命周期管理和更新蠢壹,都是通過各種代理完成的,如下圖所示九巡,上面代碼中我們用到的 ChangeNotifierProvider
大致經歷了這樣的流程:
- 設置到
ChangeNotifierProvider
的ChangeNotifer
會被執(zhí)行addListener
添加監(jiān)聽listener
图贸。 -
listener
內會調用StateDelegate
的StateSetter
方法,從而調用到StatefulWidget
的setState
冕广。 - 當我們執(zhí)行
ChangeNotifer
的notifyListeners
時疏日,就會最終觸發(fā)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
狀態(tài)共享肯定需要 InheritedWidget
瓣喊,InheritedProvider
就是InheritedWidget
的子類,所有的 Provider
實現(xiàn)都在 build
方法中使用 InheritedProvider
進行嵌套黔酥,實現(xiàn) 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
中的“登記邏輯”狸吞,從而控制了狀態(tài)改變時勉耀,需要更新的精細度。
同時庫內還提供了 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
只支持一種類型枢纠,多個時要不就是多個狀態(tài)合并到一個實體,要不就需要多個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 | 狀態(tài)管理指南篇——Provider》 已經寫過燎猛,我就不重復寫輪子了恋捆,感興趣的可以過去看看。
自此重绷,第十五篇終于結束了沸停!(///▽///)
資源推薦
- 本文Demo :https://github.com/CarGuo/state_manager_demo
- Github : https://github.com/CarGuo/
- 開源 Flutter 完整項目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開源 Flutter 多案例學習型項目: https://github.com/CarGuo/GSYFlutterDemo
- 開源 Fluttre 實戰(zhàn)電子書項目:https://github.com/CarGuo/GSYFlutterBook