重中之重, 源碼解析: (state的demo和源碼分析重點看看)
目錄
- StatelessWidget 與 StatefulWidget 區(qū)別? build方法
- 原始setstate的原理,狀態(tài)管理的重要性 (重點)
- 官方 provider的原理解析 (重點)
3.1重點的2種方式和源碼分析 , (provider和InheritedWidget)
3.2Provider的案例使用ChangeNotifier, ChangeNotifierProvider 實現(xiàn)2個組件之間的通信
3.3Consumer 刷新指定區(qū)域
3.4 Selector的用法
3.5MultiProvider 多zhuang狀態(tài)共享.
事實上力喷,當我們使用 Provider 后莉掂,我們就再也不需要使用 StatefulWidget 了。 - 手寫簡單版provider
- 基礎(chǔ)的跨組件框架, InheritedWidget、Notification和EventBus
- 第三方框架圖文比較, 咸魚Fish Redux的框架 , getX的狀態(tài)管理
狀態(tài)管理是什么:
Flutter的狀態(tài)可以分為全局狀態(tài)和局部狀態(tài)兩種指蚁。
Flutter 狀態(tài)管理是指在 Flutter 應(yīng)用中有效地管理應(yīng)用的數(shù)據(jù)和狀態(tài)
狀態(tài)管理是聲明式編程非常重要的一個概念
問題: 為什么要做狀態(tài)管理?
就是有幾個頁面, 要實現(xiàn)數(shù)據(jù)的同步或者共享!
下面是官方給出的一些原則可以幫助你做決定:
- 如果狀態(tài)是用戶數(shù)據(jù)厕九,如復選框的選中狀態(tài)、滑塊的位置除师,則該狀態(tài)最好由父 Widget 管理沛膳。
- 如果狀態(tài)是有關(guān)界面外觀效果的,例如顏色汛聚、動畫锹安,那么狀態(tài)最好由 Widget 本身來管理。
- 如果某一個狀態(tài)是不同 Widget 共享的則最好由它們共同的父 Widget 管理。
1. StatelessWidget 與 StatefulWidget 區(qū)別?
StatelessWidget和StatefulWidget的區(qū)別就在這個可變的State了叹哭。
當狀態(tài)數(shù)據(jù)發(fā)生變化時忍宋,F(xiàn)lutter 會調(diào)用 build() 方法重新構(gòu)建界面
問題: build方法什么情況下被執(zhí)行呢?:
- 1)风罩、當我們的StatelessWidget第一次被插入到Widget樹中時(也就是第一次被創(chuàng)建時)糠排;
- 2)、當我們的父Widget(parent widget)發(fā)生改變時超升,子Widget會被重新構(gòu)建入宦;
- 3)、如果我們的Widget依賴InheritedWidget的一些數(shù)據(jù)室琢,InheritedWidget數(shù)據(jù)發(fā)生改變時云石;
Stateful widget特有:
至少由兩個類組成:
一個StatefulWidget類。
一個 State類研乒; StatefulWidget類本身是不變的汹忠,但是State類中持有的狀態(tài)在 widget 生命周期中可能會發(fā)生變化。
問題: 為什么要將 build 方法放在 State 中雹熬,而不是放在StatefulWidget中宽菜?
1).狀態(tài)訪問不便, 屬性會被公開
試想一下,如果我們的StatefulWidget
有很多狀態(tài)竿报,而每次狀態(tài)改變都要調(diào)用build
方法铅乡,由于狀態(tài)是保存在 State 中的,如果build
方法在StatefulWidget
中烈菌,那么build
方法和狀態(tài)分別在兩個類中阵幸,那么構(gòu)建時讀取狀態(tài)將會很不方便!
試想一下芽世,如果真的將build
方法放在 StatefulWidget 中的話挚赊,由于構(gòu)建用戶界面過程需要依賴 State,所以build
方法將必須加一個State
參數(shù)济瓢,大概是下面這樣:
Widget build(BuildContext context, State state){
//state.counter
...
}
這樣的話就只能將State的所有狀態(tài)聲明為公開的狀態(tài)荠割,這樣才能在State類外部訪問狀態(tài)!但是旺矾,將狀態(tài)設(shè)置為公開后蔑鹦,狀態(tài)將不再具有私密性,這就會導致對狀態(tài)的修改將會變的不可控箕宙。但如果將build()方法放在State中的話嚎朽,構(gòu)建過程不僅可以直接訪問狀態(tài),而且也無需公開私有狀態(tài)柬帕,這會非常方便哟忍。
2.繼承StatefulWidget不便室囊。
例如,F(xiàn)lutter 中有一個動畫 widget 的基類AnimatedWidget魁索,它繼承自StatefulWidget類融撞。AnimatedWidget中引入了一個抽象方法build(BuildContext context),繼承自AnimatedWidget的動畫 widget 都要實現(xiàn)這個build方法〈治担現(xiàn)在設(shè)想一下尝偎,如果StatefulWidget 類中已經(jīng)有了一個build方法,正如上面所述鹏控,此時build方法需要接收一個 State 對象致扯,這就意味著AnimatedWidget必須將自己的 State 對象(記為_animatedWidgetState)提供給其子類,因為子類需要在其build方法中調(diào)用父類的build方法当辐,代碼可能如下:
問題: 為什么flutter在設(shè)計的時候statefulWidget的build方法放在state中?
- build依賴state中的變量
2.widget會不停的銷毀 - 專題改變, 不希望把state改變
問題: build的context是什么
在StatelessElement中抖僵,我們發(fā)現(xiàn)是將this傳入,所以本質(zhì)上BuildContext就是當前的Element
context是當前 widget 在 widget 樹中位置中執(zhí)行”相關(guān)操作“的一個句柄(handle)缘揪,比如它提供了從當前 widget 開始向上遍歷 widget 樹以及按照 widget 類型查找父級 widget 的方法
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context測試"),
),
body: Container(
child: Builder(builder: (context) {
// 在 widget 樹中向上查找最近的父級`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title耍群, 此處實際上是Text("Context測試")
return (scaffold.appBar as AppBar).title;
}),
),
);
// 查找父級最近的Scaffold對應(yīng)的ScaffoldState對象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
如果 StatefulWidget 的狀態(tài)是希望暴露出的,應(yīng)當在 StatefulWidget 中提供一個of 靜態(tài)方法來獲取其 State 對象找筝,開發(fā)者便可直接通過該方法來獲鹊腹浮;如果 State不希望暴露袖裕,則不提供of方法
// 直接通過of靜態(tài)方法來獲取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
StatelessWidget特有
問題: 我之前說過定義到Widget中的數(shù)據(jù)都是不可變的曹抬,必須定義為final,為什么呢急鳄?
Flutter如何做到我們在開發(fā)中定義到Widget中的數(shù)據(jù)一定是final的呢谤民?
@immutable
abstract class Widget extends DiagnosticableTree {
// ...省略代碼
}
這里有一個很關(guān)鍵的東西@immutable
我們似乎在Dart中沒有見過這種語法,這實際上是一個
注解
疾宏,這設(shè)計到Dart的元編程张足,我們這里不展開講;說明: 被@immutable注解標明的類或者子類都必須是不可變的
2. 原始setstate的原理,狀態(tài)管理的重要性
源碼分析:
setState
僅在本地范圍內(nèi)有效灾锯,如果一個 Widget
需要改變它自己的狀態(tài)兢榨,那么 setState
就是你最好的選擇
setstate() 主要用于修改數(shù)據(jù),變量值的! 相當于notifychange
問題: 如何從statefullWidget把數(shù)據(jù)傳遞到state類中?
可以2次傳遞
還有一種方法,state中可以直接取到statefullWidget的實例_widget , 就可以!
之前最簡單的就是 widget, setstate
那 State 是在哪里被創(chuàng)建的?
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
assert(() {
if (!state._debugTypesAreRight(widget)) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
ErrorDescription(
'The createState function for ${widget.runtimeType} returned a state '
'of type ${state.runtimeType}, which is not a subtype of '
'State<${widget.runtimeType}>, violating the contract for createState.',
),
]);
}
return true;
}());
assert(state._element == null);
state._element = this;
state._widget = widget;
assert(state._debugLifecycleState == _StateLifecycle.created);
}
更新ui, state是啥?
@override
void update(ProxyWidget newWidget) {
final ProxyWidget oldWidget = widget as ProxyWidget;
assert(widget != newWidget);
super.update(newWidget);
assert(widget == newWidget);
updated(oldWidget);
rebuild(force: true);
}
調(diào)用widget的setstate方法沒, 會執(zhí)行StatefulElement的update()方法
@protected
void setState(VoidCallback fn) {
assert(() {
final Object? result = fn() as dynamic;
}());
_element!.markNeedsBuild(); // markNeedsBuild ()
}
void markNeedsBuild() {
assert(_lifecycleState != _ElementLifecycle.defunct);
if (_lifecycleState != _ElementLifecycle.active) {
return;
}
assert(owner != null);
assert(_lifecycleState == _ElementLifecycle.active);
assert(() {
if (owner!._debugBuilding) {
assert(owner!._debugCurrentBuildTarget != null);
assert(owner!._debugStateLocked);
if (_debugIsInScope(owner!._debugCurrentBuildTarget!)) {
return true;
}
final List<DiagnosticsNode> information = <DiagnosticsNode>[
ErrorSummary('setState() or markNeedsBuild() called during build.'),
ErrorDescription(
),
describeElement('The widget on which setState() or markNeedsBuild() was called was'),
];
return true;
}());
if (dirty) { // 這里進行了返回!
return;
}
_dirty = true;
owner!.scheduleBuildFor(this); // 調(diào)用scheduleBuildFor方法, 傳入當前Element對象
}
void scheduleBuildFor(Element element) {
assert(element.owner == this);
assert(() {
if (debugPrintScheduleBuildForStacks) {
debugPrintStack(label: 'scheduleBuildFor() called for $element${_dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}');
}
return true;
}());
if (element._inDirtyList) {
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
_dirtyElements.add(element); //添加到_dirtyElements集合中
element._inDirtyList = true;
return true;
}());
}
從現(xiàn)象推斷顺饮,整個流程必然會經(jīng)過setState()-···················->當前State的build()-················->頁面繪制-············->屏幕刷新
問題: setState ()是如何更新UI的?
setState源碼分析總結(jié):
我們常說的 setState ,其實是調(diào)用了 markNeedsBuild 凌那,markNeedsBuild 內(nèi)部會標記 element 為 diry兼雄,添加到BuildOwner對象的_dirtyElements集合中, 然后調(diào)用scheduleFrame來注冊Vsync回調(diào)。 當下一次vsync信號的到來時會執(zhí)行handleBeginFrame()和handleDrawFrame()來更新UI帽蝶。
然后在下一幀 WidgetsBinding.drawFrame 才會被繪制赦肋,這可以也看出 setState 并不是立即生效的
State#setState 的核心作用就是把持有的元素標臟并申請新幀調(diào)度。而只有新幀到來,執(zhí)行完構(gòu)建之后佃乘,元素的 dirty 才會置為 false 囱井。也就是說,兩幀之間趣避,無論調(diào)用多少次 setState 庞呕,都只會觸發(fā)一次程帕, 元素標臟 和 申請新幀調(diào)度 。這就是為什么連續(xù)觸發(fā) 1000000 次愁拭,并無大事發(fā)生的原因
setState()會重建, 但是有dirty的判讀, 不會經(jīng)常重建!
dirty state的含義是臟的State
它實際是通過一個Element的東西(我們還沒有講到Flutter繪制原理)的屬性來標記的;
將它標記為dirty會等待下一次的重繪檢查盏混,強制調(diào)用build方法來構(gòu)建我們的Widget
setState可以分為兩個部分:
將element標臟
渲染時將所有臟element都rebuild惜论,且將自己的child進行update
重要的方法: performRebuild()
updateChild(_child, built, slot)
問題: setState每次都會去執(zhí)行build ()?
父widget
父widget2
子widget3: 用state變量
點擊: 調(diào)用setstate方法, 然后會創(chuàng)建, 調(diào)用 build(). 是不是只重繪制子widget3, 其他的不會重新繪制
setState 觸發(fā)了對你當前所在的小組件的重建。如果你的整個應(yīng)用程序只包含一個widget来涨,那么整個widget將被重建,這將使你的應(yīng)用程序變得緩慢
要把setstate多封裝一層, 讓setstate咋你自己的widget, 這樣就不會重建整個widget
問題: 為什么高位置的setState ()會消耗性能?
雖然setState的調(diào)用并沒有像 Widget 層那樣技羔,在渲染控制層的 Element 那一層重新構(gòu)建全部element。但是藤滥,這并不代表 setState 的使用沒問題社裆,首先拙绊,像之前篇章說的那樣,它會重新構(gòu)建整個 Widget 樹泳秀,這會帶來性能損耗标沪;其次,由于整個 Widget 樹改變了嗜傅,意味著整棵樹對應(yīng)的渲染層Element對象都會執(zhí)行 update方法金句,雖然不一定會重新渲染,但是這整棵樹的遍歷的性能開銷也很高
總結(jié): 雖然每次不會都創(chuàng)建element, 但是遍歷有損耗, 提升辦法: 只更新需要更新的widget!
當我們在一個高節(jié)點調(diào)用setState()的時候會構(gòu)建再次build所有的Widget吕嘀,雖然不一定掛載到Element樹中违寞,但是平時我們使用的Widget中往往嵌套多個其他類型的Widget贞瞒,每個build()方法走下來最終也會帶來不小的開銷,因此通過各種狀態(tài)管理方案趁曼,Stream等方式军浆,只做局部刷新,是我們?nèi)粘i_發(fā)中應(yīng)該養(yǎng)成的良好習慣挡闰。
流轉(zhuǎn)圖:
setState() 添加到_dirtyElements集合中, owner.scheduleBuildFor(this)
rebuild()
StatefulElement-->performRebuild(): 最重要的方法
StatefulElement--->updateChild()
Element---> update();
build()過程雖然只是調(diào)用一個組件的構(gòu)造方法乒融,不涉及對Element樹的掛載操作。
但因為我們一個組件往往是N多個Widget的嵌套組合尿这,每個都遍歷一遍開銷算下來并不小
問題: setState()如何做優(yōu)化?
要在子控件中調(diào)用setState(),
局部刷新 , 但是局部刷新簇抵,也只是 setState
的封裝
3. 官方 provider: 復雜情況的狀態(tài)管理
**數(shù)據(jù)傳遞: 多個頁面之間的傳遞, 數(shù)據(jù)變化通知! **
Flutter 官方的狀態(tài)管理框架 Provider 則相對簡單得多
Provider 是一個用來提供數(shù)據(jù)的框架。它是 InheritedWidget 的語法糖射众,提供了依賴注入的功能碟摆,允許在 Widget 樹中更加靈活地處理和傳遞數(shù)據(jù)
Provider 就是針對 InheritedWidget
的一個包裝工具
在使用Provider的時候,我們主要關(guān)心三個概念:
- ChangeNotifier:真正數(shù)據(jù)(狀態(tài))存放的地方
- ChangeNotifierProvider:Widget樹中提供數(shù)據(jù)(狀態(tài))的地方叨橱,會在其中創(chuàng)建對應(yīng)的ChangeNotifier
- Consumer:Widget樹中需要使用數(shù)據(jù)(狀態(tài))的地方 讀Provider
讀和寫的Provider
ChangeNotifierProvider:Widget樹中提供數(shù)據(jù)(狀態(tài))的地方典蜕,會在其中創(chuàng)建對應(yīng)的ChangeNotifier
但是,濫用 Provider.of 方法也有副作用罗洗,那就是當數(shù)據(jù)更新時愉舔,頁面中其他的子 Widget 也會跟著一起刷新。
可以看到轩缤,TestIcon 控件本來是一個不需要刷新的 StatelessWidget火的,但卻因為其父 Widget FloatingActionButton 所依賴的數(shù)據(jù)資源 counter 發(fā)生了變化馏鹤,導致它也要跟著刷新湃累。
Consumer: 消費, 把數(shù)據(jù)消費! (用的比 Provider.of 多)
因為Consumer在刷新整個Widget樹時治力,會盡可能少的rebuild Widget琴许。
在數(shù)據(jù)資源發(fā)生變化時榜田,只刷新對資源存在依賴關(guān)系的 Widget箭券,而其他 Widget 保持不變呢?
有一個child().不重新構(gòu)建
Consumer是否是最好的選擇呢荆永?并不是豆村,它也會存在弊端 Selector的選擇 (使用的比較多)
比如當點擊了floatingActionButton時,我們在代碼的兩處分別打印它們的builder是否會重新調(diào)用薇正;
我們會發(fā)現(xiàn)只要點擊了floatingActionButton眷射,兩個位置都會被重新builder;
但是floatingActionButton的位置有重新build的必要嗎苦囱?沒有鱼鸠,因為它是否在操作數(shù)據(jù)蚀狰,并沒有展示麻蹋;
如何可以做到讓它不要重新build了扮授?使用Selector來代替Consumer
所以在某些情況下,我們可以使用Selector來代替Consumer堪侯,性能會更高
Selector和Consumer對比伍宦,不同之處主要是三個關(guān)鍵點:
關(guān)鍵點1:泛型參數(shù)是兩個
泛型參數(shù)一:我們這次要使用的Provider
泛型參數(shù)二:轉(zhuǎn)換之后的數(shù)據(jù)類型,比如我這里轉(zhuǎn)換之后依然是使用CounterProvider遇骑,那么他們兩個就是一樣的類型
關(guān)鍵點2:selector回調(diào)函數(shù)
轉(zhuǎn)換的回調(diào)函數(shù)势篡,你希望如何進行轉(zhuǎn)換
S Function(BuildContext, A) selector
我這里沒有進行轉(zhuǎn)換禁悠,所以直接將A實例返回即可
關(guān)鍵點3:是否希望重新rebuild
這里也是一個回調(diào)函數(shù)碍侦,我們可以拿到轉(zhuǎn)換前后的兩個實例瓷产;
bool Function(T previous, T next);
因為這里我不希望它重新rebuild濒旦,無論數(shù)據(jù)如何變化尔邓,所以這里我直接return false梯嗽;
多狀態(tài)的資源封裝
Provider 的另一個升級版 MultiProvider灯节,來實現(xiàn)多個 Provider 的組合注入
ScrollAwareImageProvider:
ChangeNotifier:
搞清楚TextField是怎么使用ChangeNotifier的了卡骂,為什么每次改變TextEditingController的text值偿警,然后在TextField數(shù)據(jù)框里的數(shù)據(jù)也及時改變了,其實最后還是用到setState盒使。
原理:
performRebuild() :該回調(diào)會在setState或者build的時候會觸發(fā)苞慢;此處做了一個判斷挽放,只會在第一次build的時候觸發(fā) build()
notifyDependent()
問題: InheritedElement? 而不是用普通的Element?
如Flutter SDK中正是通過InheritedWidget來共享應(yīng)用主題(Theme)和Locale (當前語言環(huán)境)信息
InheritedWidget: 作為根節(jié)點, 然后其他子節(jié)點, 就可以獲取根節(jié)點的狀態(tài)! 但是如果要更新的話, 還是的手動用setstate
最后辑畦,我們重寫了 updateShouldNotify 方法纯出,這個方法會在 Flutter 判斷 InheritedWidget 是否需要重建暂筝,從而通知下層觀察者組件更新數(shù)據(jù)時被調(diào)用到焕襟。在這里鸵赖,我們直接判斷 count 是否相等即可卫漫。
定義一個共享數(shù)據(jù)的InheritedWidget列赎,需要繼承自InheritedWidget
- 這里定義了一個of方法包吝,該方法通過context開始去查找祖先的HYDataWidget(可以查看源碼查找過程)
- updateShouldNotify方法是對比新舊HYDataWidget诗越,是否需要對更新相關(guān)依賴的Widget
InheritedWidget中的屬性在子Widget中只能讀嚷狞,如果有修改的場景床未,我們需要把它和StatefulWidget中的State配套使用薇搁。
InheritedWidget是Flutter中的一個功能型Widget啃洋,適用于在Widget樹中共享數(shù)據(jù)的場景宏娄。通過它绝编,我們可以高效地將數(shù)據(jù)在Widget樹中進行跨層傳遞
InheritedWidget使用方法
可以看到InheritedWidget的使用方法還是比較簡單的窟勃,無論Counter在CountContainer下層什么位置逗堵,都能獲取到其父Widget的計數(shù)屬性count蜒秤,再也不用手動傳遞屬性了作媚。
不過纸泡,InheritedWidget僅提供了數(shù)據(jù)讀的能力蚤假,如果我們想要修改它的數(shù)據(jù)磷仰,則需要把它和StatefulWidget中的State配套使用
使用場景:
InheritedWidget的數(shù)據(jù)流動方式是從父Widget到子Widget逐層傳遞
InheritedWidget共有兩個方法
1).createElement() (創(chuàng)建對應(yīng)的Element)
2).updateShouldNotify(covariant InheritedWidget oldWidget)
問題: Flutter中的InheritedWidget的實現(xiàn)原理是怎么樣的?
InheritedWidget的原理:
主要是觀察模式的思想
源碼分析: (重點)
class CountContainer extends InheritedWidget {
// 方便其子 Widget 在 Widget 樹中找到它
static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
final int count;
CountContainer({
Key key,
@required this.count,
@required Widget child,
}): super(key: key, child: child);
// 判斷是否需要更新
@override
bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
}
Provider 特點:
因為 Provider 實際上是 InheritedWidget 的語法糖伺通,所以通過 Provider 傳遞的數(shù)據(jù)從數(shù)據(jù)流動方向來看民逼,是由父到子(或者反過來)。這時我們就明白了疮鲫,原來需要把資源放到 FirstPage 和 SecondPage 的父 Widget弦叶,也就是應(yīng)用程序的實例 MyApp 中(當然伤哺,把資源放到更高的層級也是可以的,比如放到 main 函數(shù)中)
他最主要的功能:就是會調(diào)用Element的performRebuild()方法绢彤,然后觸發(fā)ComponentElement的build()方法,最終觸發(fā)_InheritedProviderScopeElement的build方法
其他: markNeedsNotifyDependents, 我們使用 notifyListeners()饶氏,就會觸發(fā)疹启,這個回調(diào)
provide不是調(diào)用的setstate()進行狀態(tài)管理的么?
那怎么會觸發(fā)到performRebuild()這個方法了?
當我們執(zhí)行 ChangeNotifer 的 notifyListeners 時喊崖,就會最終觸發(fā) setState 更新贷祈。
執(zhí)行流程呜达。
ChangeNotifer------>notifyListeners
setState()
InheritedElement------>performRebuild
InheritedElement------>build
InheritedElement------>notifyListeners
我們要知道一個前提:刷新Widget會先進入Element的rebuild方法查近。然后是performRebuild方法霜威,這個方法Element沒做什么,交由具體子類去實現(xiàn)
provider代碼原理總結(jié):
wiget是InheritedWidget
1)大猛、 Provider 的內(nèi)部 DelegateWidget 是一個 StatefulWidget 挽绩,所以可以更新且具有生命周期唉堪。
2)唠亚、狀態(tài)共享是使用了 InheritedProvider 這個 InheritedWidget 實現(xiàn)的趾撵。
4.手寫簡單版provider
使用
view
class CounterEasyPPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierEasyP(
create: (BuildContext context) => CounterEasyP(),
builder: (context) => _buildPage(context),
);
}
Widget _buildPage(BuildContext context) {
final easyP = EasyP.of<CounterEasyP>(context);
return Scaffold(
appBar: AppBar(title: Text('自定義狀態(tài)管理框架-EasyP范例')),
body: Center(
child: EasyPBuilder<CounterEasyP>(() {
return Text(
'點擊了 ${easyP.count} 次',
style: TextStyle(fontSize: 30.0),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => easyP.increment(),
child: Icon(Icons.add),
),
);
ChangeNotifier
class CounterEasyP extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners();
}
}
核心類: 3個
ChangeNotifierProvider: 最重要的方法
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatelessWidget {
ChangeNotifierEasyP({
Key? key,
required this.create,
this.builder,
this.child,
}) : super(key: key);
final T Function(BuildContext context) create;
final Widget Function(BuildContext context)? builder;
final Widget? child;
@override
Widget build(BuildContext context) { // 重寫 build()方法, 返回InheritedWidget對象
assert(
builder != null || child != null,
'$runtimeType must specify a child',
);
return EasyPInherited(
create: create,
child: builder != null
? Builder(builder: (context) => builder!(context))
: child!,
);
}
}
class EasyPInherited<T extends ChangeNotifier> extends InheritedWidget { // 實現(xiàn)一個InheritedWidget
EasyPInherited({
Key? key,
required Widget child,
required this.create,
}) : super(key: key, child: child);
final T Function(BuildContext context) create;
@override
bool updateShouldNotify(InheritedWidget oldWidget) => false; // 重寫updateShouldNotify方法
@override
InheritedElement createElement() => EasyPInheritedElement(this);
}
class EasyPInheritedElement<T extends ChangeNotifier> extends InheritedElement { // InheritedElement重寫
EasyPInheritedElement(EasyPInherited<T> widget) : super(widget);
bool _firstBuild = true;
bool _shouldNotify = false;
late T _value;
late void Function() _callBack;
T get value => _value;
@override
void performRebuild() { // 實現(xiàn)performRebuild()
if (_firstBuild) {
_firstBuild = false;
_value = (widget as EasyPInherited<T>).create(this);
_value.addListener(_callBack = () {
// 處理刷新邏輯究珊,此處無法直接調(diào)用notifyClients
// 會導致owner!._debugCurrentBuildTarget為null剿涮,觸發(fā)斷言條件取试,無法向后執(zhí)行
_shouldNotify = true;
markNeedsBuild();
});
}
super.performRebuild();
}
@override
Widget build() { // build()重寫
if (_shouldNotify) {
_shouldNotify = false;
notifyClients(widget as EasyPInherited<T>);
}
return super.build();
}
@override
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
//此處就直接刷新添加的監(jiān)聽子Element了,不各種super了
dependent.markNeedsBuild();
// super.notifyDependent(oldWidget, dependent);
}
@override
void unmount() {
_value.removeListener(_callBack);
_value.dispose();
super.unmount();
}
}
Provider
class Provider { // 不包含核心邏輯,僅僅是封裝
/// 獲取EasyP實例
/// 獲取實例的時候,listener參數(shù)老是寫錯,這邊直接用倆個方法區(qū)分了
static T of<T extends ChangeNotifier>(BuildContext context) {
return _getInheritedElement<T>(context).value;
}
/// 注冊監(jiān)聽控件
static T register<T extends ChangeNotifier>(BuildContext context) {
var element = _getInheritedElement<T>(context);
context.dependOnInheritedElement(element);
return element.value;
}
/// 獲取距離當前Element最近繼承InheritedElement<T>的組件
//調(diào)用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的區(qū)別就是前者會注冊依賴關(guān)系初婆,而后者不會
static EasyPInheritedElement<T>
_getInheritedElement<T extends ChangeNotifier>(BuildContext context) {
var inheritedElement = context
.getElementForInheritedWidgetOfExactType<EasyPInherited<T>>()
as EasyPInheritedElement<T>?;
if (inheritedElement == null) {
throw EasyPNotFoundException(T);
}
return inheritedElement;
}
}
class EasyPNotFoundException implements Exception {
EasyPNotFoundException(this.valueType);
final Type valueType;
@override
String toString() => 'Error: Could not find the EasyP<$valueType>';
}
builder :
class EasyPBuilder<T extends ChangeNotifier> extends StatelessWidget {
const EasyPBuilder(
this.builder, {
Key? key,
}) : super(key: key);
final Widget Function() builder;
@override
Widget build(BuildContext context) {
EasyP.register<T>(context);
return builder();
}
}
案例:
自己寫一個封裝的controller()
3個核心類:
build:
Provider:
ChangeNotifierProvider:
點擊事件: 數(shù)據(jù)變化調(diào)用provide里面的變化方法,
ui更新, 也是拿provider的值
Widget _buildPage(BuildContext context) {
final easyP = EasyP.of<CounterEasyP>(context);
return Scaffold(
appBar: AppBar(title: Text('自定義狀態(tài)管理框架-EasyP范例')),
body: Center(
child: EasyPBuilder<CounterEasyP>(() {
return Text(
'點擊了 ${easyP.count} 次',
style: TextStyle(fontSize: 30.0),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => easyP.increment(),
child: Icon(Icons.add),
),
);
}
}
創(chuàng)建了一個widget:
class CounterEasyPPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierEasyP(
create: (BuildContext context) => CounterEasyP(),
builder: (context) => _buildPage(context),
);
}
把需要更新的widget傳入到構(gòu)造方法中去了! ChangeNotifier傳入, buildcontext也在
5. 簡單的狀態(tài)管理:
對于數(shù)據(jù)的跨層傳遞弊琴,F(xiàn)lutter還提供了三種方案:InheritedWidget敲董、Notification和EventBus臣缀。接下來,我將依次為你講解這三種方案锣杂。
5.1 InheritedWidget 原理如上
5.2 Notification
數(shù)據(jù)流動方式是從子Widget向上傳遞至父Widget。這樣的數(shù)據(jù)傳遞機制適用于子Widget狀態(tài)變更踱蠢,發(fā)送通知上報的場景
Notification
是一種用于在小部件樹中傳遞信息的機制棋电,它可以用于實現(xiàn)子樹中的特定部分之間的通信茎截。Notification
并不像狀態(tài)管理或全局狀態(tài)傳遞那樣普遍,它主要用于特定場景下的通信赶盔,比如當某個事件發(fā)生時企锌,需要在小部件樹的各個部分之間傳遞消息。Notification
的工作方式是通過Notification
對象在小部件樹中傳遞于未,然后從父級小部件開始逐級向上冒泡撕攒,直到找到一個處理該通知的小部件為止陡鹃。每個處理通知的小部件可以根據(jù)需要執(zhí)行特定的操作。你可以把InheritedWidget 理解為從上到下傳遞抖坪、共享的方式脊阴,而Notification
則是從下往上状知。Notification
它提供了dispatch
方法,沿著context對應(yīng)的Element節(jié)點向上逐層發(fā)送通知。
跨組件事件傳遞
5.3 EventBus
在組件之間如果有事件需要傳遞,一方面可以一層層來傳遞,另一方面我們也可以使用一個EventBus工具來完成雄人。
其實EventBus在Vue、React中都是一種非常常見的跨組件通信的方式:
EventBus相當于是一種訂閱者模式,通過一個全局的對象來管理翻具;
這個EventBus我們可以自己實現(xiàn)工禾,也可以使用第三方的EventBus槽畔;
無需發(fā)布者與訂閱者之間存在父子關(guān)系的數(shù)據(jù)同步機制早直。
無論是InheritedWidget還是Notificaiton祥得,它們的使用場景都需要依靠Widget樹,也就意味著只能在有父子關(guān)系的Widget之間進行數(shù)據(jù)共享。但是,組件間數(shù)據(jù)傳遞還有一種常見場景:這些組件間不存在父子關(guān)系。這時,事件總線EventBus就登場了闪萄。
事件總線是在Flutter中實現(xiàn)跨組件通信的機制三椿。它遵循發(fā)布/訂閱模式蛋叼,允許訂閱者訂閱事件,當發(fā)布者觸發(fā)事件時,訂閱者和發(fā)布者之間可以通過事件進行交互砚哆。發(fā)布者和訂閱者之間無需有父子關(guān)系关炼,甚至非Widget對象也可以發(fā)布/訂閱社痛。這些特點與其他平臺的事件總線機制是類似的。
總結(jié):
這里我準備了一張表格匈织,把屬性傳值掸哑、InheritedWidget择浊、Notification與EventBus這四種數(shù)據(jù)共享方式的特點和使用場景做了簡單總結(jié)糕篇,供你參考:
6. 下面是對Provider、BLoC余舶、Redux绊谭、GetX蒙保、Riverpod和MobX等Flutter狀態(tài)管理庫的一些對比:
`1). InheritedWidget:在 Flutter 中硅堆,所有 Widget 都是通過父 Widget 構(gòu)建出來的哭廉,父 Widget 可以通過 InheritedWidget 共享狀態(tài)給子 Widget,子 Widget 可以通過調(diào)用 InheritedWidget.of() 方法來獲取共享的狀態(tài)乌企。
2). Provider:
- 適用場景: 適用于中小型應(yīng)用,需要在多個層級共享狀態(tài)的場景脖岛。
- 特點: 輕量級,使用InheritedWidget來共享狀態(tài),支持各種類型的狀態(tài)质帅,易于上手。
- 優(yōu)勢: 簡單易用登渣,不需要大量的額外代碼,具有高性能毡泻,適用于簡單的狀態(tài)共享胜茧。
- 劣勢: 在大型應(yīng)用中可能難以管理復雜的狀態(tài)。
狀態(tài)管理混亂仇味,雖然用了 provider
來做狀態(tài)管理呻顽,但一些代碼如:異步請求、事件響應(yīng)等還是會摻雜在UI頁面的代碼里面丹墨,一旦頁面的各種 Widget
多了起來之后廊遍,顯得非常嚴重,而且對業(yè)務(wù)邏輯的測試也不方便贩挣,多個組件可能需要共享相同的數(shù)據(jù)或狀態(tài)喉前,需要在每個組件中分別創(chuàng)建 Provider
實例,容易導致代碼冗余揽惹,如果只需要更新頁面的部分 Widget
使用Provider
還會導致嵌套過深被饿,也可能導致性能問題、狀態(tài)不一致以及難以追蹤的錯誤
3). ScopedModel:ScopedModel 也可以實現(xiàn)狀態(tài)共享搪搏,但它的思想是將數(shù)據(jù)放在一個共享的 Model 中狭握,然后讓需要用到這些數(shù)據(jù)的 Widget 注冊監(jiān)聽該 Model,當 Model 的數(shù)據(jù)改變時疯溺,通知監(jiān)聽它的 Widget 更新论颅。
4). Stream
Stream是一種用于在應(yīng)用程序中管理狀態(tài)和數(shù)據(jù)流的重要工具。Stream是異步數(shù)據(jù)流的抽象表示囱嫩,它可以在應(yīng)用程序中傳遞和監(jiān)聽數(shù)據(jù)的變化恃疯。但是它和Flutter關(guān)系并不大,它是通過純dart去實現(xiàn)的墨闲。你可以理解為flutter只是通過StreamBuilder
去構(gòu)建了一個Stream
通道今妄。它的使用其實也并沒有復雜太多,通常只需要創(chuàng)建StreamController
鸳碧,然后去監(jiān)聽控制器(可以直接去監(jiān)聽StreamController
盾鳞,然后通過setState更新UI,也可以通過StreamBuilder
)瞻离,最后將更新后的數(shù)據(jù)通過Stream的sink屬性添加到Stream中即可腾仅。知名的狀態(tài)管理庫Bloc,就是基于Stream的封裝套利。
5). BLoC:
BLoC 算是 Flutter 早期比較知名的狀態(tài)管理框架, 它是基于事件驅(qū)動來實現(xiàn)的狀態(tài)管理
BLoC 是業(yè)務(wù)邏輯組件的縮寫推励,它使用 Streams 和Provider 庫將業(yè)務(wù)邏輯和 UI 分離開來鹤耍,可以用來管理狀態(tài)和處理用戶輸入。作者在這里使用了Bloc用于狀態(tài)管理
基于 Stream
的封裝可以更方便做一些事件狀態(tài)的監(jiān)聽和轉(zhuǎn)換
- 適用場景: 適用于復雜的應(yīng)用验辞,需要分離業(yè)務(wù)邏輯和UI的場景稿黄。
- 特點: 通過Streams管理狀態(tài)和業(yè)務(wù)邏輯,將界面層與業(yè)務(wù)邏輯層分開跌造,適合中大型應(yīng)用抛猖。
- 優(yōu)勢: 適合處理復雜的狀態(tài)變化和異步操作,便于測試和維護鼻听。
- 劣勢: 在簡單應(yīng)用中可能顯得過于復雜。需要寫更多的代碼联四,開發(fā)節(jié)奏會有點影響
6). Redux:
- Redux 是一種狀態(tài)管理模式撑碴,它將狀態(tài)和狀態(tài)更新封裝在一個可預測的單向數(shù)據(jù)流中,可以用于處理應(yīng)用程序的復雜狀態(tài)朝墩。
前端開始者對 redux 可能會更熟悉一些
在 flutter_redux 中醉拓,開發(fā)者的每個操作都只是一個 Action
,而這個行為所觸發(fā)的邏輯完全由 middleware
和 reducer
決定
- 適用場景: 適用于需要管理大量復雜狀態(tài)的應(yīng)用收苏。
- 特點: 基于單一狀態(tài)源和不可變狀態(tài)亿卤,通過Actions和Reducers來管理狀態(tài)變化。
- 優(yōu)勢: 嚴格的狀態(tài)管理鹿霸,適用于大型應(yīng)用排吴,具有強大的開發(fā)工具和中間件。
- 劣勢: 在小型應(yīng)用中可能過于繁瑣懦鼠,學習曲線較陡`
7). ;不在維護了2012年后沒有維護了! 第三方框架, 咸魚Fish Redux的框架
8). Riverpod:
適用場景: 適用于需要更強大钻哩、更簡單的狀態(tài)管理和依賴注入的場景。
特點: 基于Provider的升級版本肛冶,提供更簡單街氢、更強大的API,支持多種狀態(tài)管理模式睦袖。
優(yōu)勢: 代碼清晰珊肃,性能高效,支持多種狀態(tài)管理模式馅笙,適用于各種規(guī)模的項目伦乔。
劣勢: 相對較新的庫,社區(qū)可能還在成長延蟹。
隨著 Flutter 的發(fā)展评矩,這些年 Flutter 上的狀態(tài)管理框架如“雨后春筍”般層出不窮,而近一年以來最受官方推薦的狀態(tài)管理框架無疑就是 Riverpod
直觀的就是 Riverpod 中的 Provider 可以隨意寫成全局, 如果說 Riverpod 最明顯的特點是什么阱飘,那就是外部不依賴 BuildContext
它的作者也是 Provider 的作者斥杜,同時也是 Flutter 官方推薦的狀態(tài)管理庫
如何實現(xiàn)不依賴 BuildContext?
在 Riverpod 里基本是每一個 “Provider” 都會有一個自己的 “Element” 虱颗,然后通過 WidgetRef 去 Hook 后成為 BuildContext 的替代,所以這就是 Riverpod 不依賴 Context 的 “魔法” 之一
9). GetX:
適用場景: 適用于快速開發(fā)和中小型應(yīng)用蔗喂,需要輕量級狀態(tài)管理和依賴注入的場景忘渔。
特點: 簡單易用,提供狀態(tài)管理缰儿、依賴注入和路由導航的綜合解決方案畦粮。
優(yōu)勢: 低學習曲線,高性能乖阵,適用于快速迭代的小型項目宣赔。
劣勢: 對于大型復雜應(yīng)用,可能需要更復雜的狀態(tài)管理方案瞪浸。
10). MobX:
適用場景: 適用于需要響應(yīng)式編程和可觀察對象的場景儒将。
特點: 通過可觀察對象和反應(yīng)式編程來管理狀態(tài),支持多種數(shù)據(jù)變化方式对蒲。提高開發(fā)效率
優(yōu)勢: 簡化了狀態(tài)管理钩蚊,具有響應(yīng)式編程的特點,易于學習和使用蹈矮。
劣勢: 相對較新的庫砰逻,可能在一些大型項目中缺乏一些高級功能。全家桶泛鸟,做的太多對于一些使用者來說是致命缺點蝠咆,需要解決的 Bug 也多
11). rxdart: 太老了