概述
- 為什么需要狀態(tài)管理?
- 共享狀態(tài)管理
一. 為什么需要狀態(tài)管理?
-
1.1、認(rèn)識狀態(tài)管理
很多從命令式編程框架(Android或iOS原生開發(fā)者)轉(zhuǎn)成聲明式編程(Flutter、Vue、React等)剛開始并不適應(yīng),因為需要一個新的角度來考慮APP的開發(fā)模式。Flutter作為一個現(xiàn)代的框架,是聲明式編程的:
在編寫一個應(yīng)用的過程中,我們有大量的State需要來進(jìn)行管理,而正是對這些State的改變,來更新界面的刷新:
-
1.2、不同狀態(tài)管理分類
-
1.2.1. 短時狀態(tài)Ephemeral state
某些狀態(tài)只需要在自己的Widget中使用即可- 比如我們之前做的簡單計數(shù)器counter
- 比如一個PageView組件記錄當(dāng)前的頁面
- 比如一個動畫記錄當(dāng)前的進(jìn)度
- 比如一個BottomNavigationBar中當(dāng)前被選中的tab
這種狀態(tài)我們只需要使用StatefulWidget對應(yīng)的State類自己管理即可,Widget樹中的其它部分并不需要訪問這個狀態(tài)。
-
1.2.2、應(yīng)用狀態(tài)App state
開發(fā)中也有非常多的狀態(tài)需要在多個部分進(jìn)行共享- 比如用戶一個個性化選項
- 比如用戶的登錄狀態(tài)信息
- 比如一個電商應(yīng)用的購物車
- 比如一個新聞應(yīng)用的已讀消息或者未讀消息
這種狀態(tài)我們?nèi)绻赪idget之間傳遞來、傳遞去,那么是無窮盡的,并且代碼的耦合度會變得非常高,牽一發(fā)而動全身,無論是代碼編寫質(zhì)量、后期維護(hù)、可擴(kuò)展性都非常差。
這個時候我們可以選擇全局狀態(tài)管理的方式,來對狀態(tài)進(jìn)行統(tǒng)一的管理和應(yīng)用。 -
1.2.3、如何選擇不同的管理方式
開發(fā)中,沒有明確的規(guī)則去區(qū)分哪些狀態(tài)是短時狀態(tài),哪些狀態(tài)是應(yīng)用狀態(tài)。
某些短時狀態(tài)可能在之后的開發(fā)維護(hù)中需要升級為應(yīng)用狀態(tài)。
但是我們可以簡單遵守下面這幅流程圖的規(guī)則:針對React使用setState還是Redux中的Store來管理狀態(tài)哪個更好的問題,Redux的issue上,Redux的作者Dan Abramov,它這樣回答的:
The rule of thumb is: Do whatever is less awkward
經(jīng)驗原則就是:選擇能夠減少麻煩的方式。
-
二、共享狀態(tài)管理
-
2.1、InheritedWidget
InheritedWidget和React中的context功能類似,可以實現(xiàn)跨組件數(shù)據(jù)的傳遞。
定義一個共享數(shù)據(jù)的InheritedWidget,需要繼承自InheritedWidget這里定義了一個
of方法
,該方法通過context開始去查找祖先的JKDataWidget(可以查看源碼查找過程)-
updateShouldNotify方法是對比新舊JKDataWidget,是否需要對更新相關(guān)依賴的Widget
class JKCounterWidget extends InheritedWidget { /* 1.共享的數(shù)據(jù) */ final int counter; /* 2.自定義的構(gòu)造方法 */ JKCounterWidget({this.counter, Widget child}): super(child: child); /* 3.獲取組件最近的當(dāng)前的InheritedWidget */ static JKCounterWidget of(BuildContext context) { /* 沿著Element樹去找到最近的JKJKCounterElement, 從Element里面取出Widget對象 */ return context.dependOnInheritedWidgetOfExactType(); } /* 4.決定要不要回調(diào)State中didChangeDependencies() */ /* 如果返回True, 執(zhí)行依賴當(dāng)期的InheritedWidget的State中的didChangeDependencies(),反之不會執(zhí)行didChangeDependencies() */ @override bool updateShouldNotify(InheritedWidget oldWidget) { return true; } }
創(chuàng)建JKDataWidget,并且傳入數(shù)據(jù)(這里點擊按鈕會修改數(shù)據(jù),并且重新build)
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( // 啟動要顯示的界面 home: HomePage(), ); } } class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _counter = 100; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("圖標(biāo)組件示例"), ), body: JKCounterWidget( counter: _counter, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ JKShowData01(), JKShowData02() ], ), ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { setState(() { _counter++; }); }, ), ); } } class JKShowData01 extends StatelessWidget { @override Widget build(BuildContext context) { int counter = JKCounterWidget.of(context).counter; return Card( color: Colors.brown, child: Text('當(dāng)前計數(shù):$counter', style: TextStyle(fontSize: 30, color: Colors.white),), ); } } class JKShowData02 extends StatefulWidget { @override _JKShowData02State createState() => _JKShowData02State(); } class _JKShowData02State extends State<JKShowData02> { @override void didChangeDependencies() { // TODO: implement didChangeDependencies super.didChangeDependencies(); print('執(zhí)行了'); } @override Widget build(BuildContext context) { int counter = JKCounterWidget.of(context).counter; return Card( color: Colors.green, child: Text('當(dāng)前計數(shù):$counter', style: TextStyle(fontSize: 30, color: Colors.white),), ); } }
在某個Widget中使用共享的數(shù)據(jù),并且監(jiān)聽,如上面的
JKShowData01
和JKShowData02
-
2.2、Provider
Provider是目前官方推薦的全局狀態(tài)管理工具,由社區(qū)作者Remi Rousselet 和 Flutter Team共同編寫。
使用之前,我們需要先引入對它的依賴,截止這篇文章,Provider的最新版本為4.1.2
,最新的我們可以到 https://pub.dev/ 搜所 providerdependencies: provider:^4.1.2
-
2.2.1、Provider 的基本使用
在使用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))的地方
我們先來完成一個簡單的案例,將官方計數(shù)器案例使用Provider來實現(xiàn):-
第一步:創(chuàng)建自己的ChangeNotifier
我們需要一個ChangeNotifier來保存我們的狀態(tài),所以創(chuàng)建它這里我們可以使用
繼承(extends)
自ChangeNotifier
,也可以使用混入(with)
,這取決于概率是否需要繼承自其它的類-
我們使用一個私有的_counter,并且提供了getter和setter,提示:可以使用快捷鍵來快速生成 getter和setter 方法,鼠標(biāo)放到類名上,cmd + n
-
在setter中我們監(jiān)聽到_counter的改變,就調(diào)用notifyListeners方法,通知所有的Consumer進(jìn)行更新
import 'package:flutter/material.dart'; /* with 代表混入 * Dart不支持多繼承,如果我們的類繼承多個類,那么就要使用 with * 如果我們的類沒有繼承于其他的類,那么就可以使用 extends 來繼承ChangeNotifier */ class JKCounterViewModel with ChangeNotifier { int _counter = 101; int get counter => _counter; set counter(int value) { _counter = value; /* 通知所有的監(jiān)聽 */ notifyListeners(); } }
-
第二步:在Widget Tree中插入ChangeNotifierProvider
我們需要在Widget Tree中插入ChangeNotifierProvider,以便Consumer可以獲取到數(shù)據(jù):將ChangeNotifierProvider
放到了頂層
,這樣方便在整個應(yīng)用的任何地方可以使用CounterProvidervoid main() { runApp( ChangeNotifierProvider( child: MyApp(), create: (ctx) => JKCounterViewModel(), ), ); }
-
第三步:在首頁中使用Consumer引入和修改狀態(tài)
引入位置一:在body中使用Consumer,Consumer需要傳入一個builder回調(diào)函數(shù),當(dāng)數(shù)據(jù)發(fā)生變化時,就會通知依賴數(shù)據(jù)的Consumer重新調(diào)用builder方法來構(gòu)建;
-
引入位置二:在floatingActionButton中使用Consumer,當(dāng)點擊按鈕時,修改CounterNotifier中的counter數(shù)據(jù);
class JKHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("列表測試"), ), body: Center( child: Consumer< JKCounterViewModel >( builder: (ctx, viewModel, child) { return Text("當(dāng)前計數(shù):${viewModel.counter}", style: TextStyle(fontSize: 20, color: Colors.red),); } ), ), floatingActionButton: Consumer<JKCounterViewModel>( builder: (ctx, counterPro, child) { return FloatingActionButton( child: child, onPressed: () { counterPro.counter += 1; }, ); }, child: Icon(Icons.add), ), ); } }
提示:
child: Icon(Icons.add) 和 child: child 配合使用是為了避免 Icon(Icons.add) 被重創(chuàng)建 Consumer的builder方法解析:
參數(shù)一:context,每個build方法都會有上下文,目的是知道當(dāng)前樹的位置
參數(shù)二:ChangeNotifier對應(yīng)的實例,也是我們在builder函數(shù)中主要使用的對象
參數(shù)三:child,目的是進(jìn)行優(yōu)化,如果builder下面有一顆龐大的子樹,當(dāng)模型發(fā)生改變的時候,我們并不希望重新build這顆子樹,那么就可以將這顆子樹放到Consumer的child中,在這里直接引入即可(注意我案例中的Icon所放的位置)-
步驟四:在我們抽取出來的 Widget 里面 2 種 使用方式
class JKShowData01 extends StatelessWidget { @override Widget build(BuildContext context) { int counter = Provider.of<JKCounterViewModel>(context).counter; print('---Data01 的 Build 的方法---'); return Container( color: Colors.brown, child: Text('當(dāng)前計數(shù):$counter', style: TextStyle(fontSize: 30, color: Colors.white),), ); } } class JKShowData02 extends StatelessWidget { @override Widget build(BuildContext context) { print('---Data02 的 Build 的方法---'); return Container( color: Colors.green, child: Consumer<JKCounterViewModel>( builder: (ctx, counter, child){ print("data02 Consumer build方法被執(zhí)行"); return Text('當(dāng)前計數(shù):${counter.counter}', style: TextStyle(fontSize: 30, color: Colors.white),); }, ), ); } }
提示:
- 1、JKShowData01里面直接使用
int counter = Provider.of<JKCounterViewModel>(context).counter;
,然后:直接使用 counter,缺點是:每次 counter 更新 JKShowData01 的Widget build(BuildContext context) {}
都會走,影響性能 - 2、JKShowData02里面使用
Consumer
就比較好了,JKShowData02 的Widget build(BuildContext context) {}
不會走,Consumer在刷新整個Widget樹時,會盡可能少的rebuild Widget,一定程度上提高了性能。
- 1、JKShowData01里面直接使用
-
2.2.2、Selector 的選擇
Consumer是否是最好的選擇呢?并不是,它也會存在弊端- 比如當(dāng)點擊了floatingActionButton時,我們在代碼的兩處分別打印它們的builder是否會重新調(diào)用;
- 我們會發(fā)現(xiàn)只要點擊了floatingActionButton,兩個位置都會被重新builder;
- 但是floatingActionButton的位置有重新build的必要嗎?沒有,因為它是否在操作數(shù)據(jù),并沒有展示;
- 如何可以做到讓它不要重新build了?使用Selector來代替Consumer
我們先直接實現(xiàn)代碼,在解釋其中的含義:
floatingActionButton: Selector<JKCounterViewModel, JKCounterViewModel>( selector: (ctx, counterVM) => counterVM, /* 要不要重新構(gòu)建,true 那么loatingActionButton 的 builder 方法會被執(zhí)行,反之false不會被執(zhí)行*/ shouldRebuild: (previous, next) => false, builder: (ctx, counterVM, child) { print('floatingActionButton 的 builder 方法被執(zhí)行'); return FloatingActionButton( child: child, onPressed: () { counterVM.counter += 1; }, ); }, child: Icon(Icons.add), )
提示:
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ù),你希望如何進(jìn)行轉(zhuǎn)換
- S Function(BuildContext, A) selector
- 我這里沒有進(jìn)行轉(zhuǎn)換,所以直接將A實例返回即可
- 關(guān)鍵點3:是否希望重新rebuild
- 這里也是一個回調(diào)函數(shù),我們可以拿到轉(zhuǎn)換前后的兩個實例;
bool Function(T previous, T next); - 因為這里我不希望它重新rebuild,無論數(shù)據(jù)如何變化,所以這里我直接return false;
- 這里也是一個回調(diào)函數(shù),我們可以拿到轉(zhuǎn)換前后的兩個實例;
這個時候,我們重新測試點擊floatingActionButton,floatingActionButton中的代碼并不會進(jìn)行rebuild操作。所以在某些情況下,我們可以使用Selector來代替Consumer,性能會更高。
-
建議:
只是依賴數(shù)據(jù)一般使用 Consumer;如果不是依賴數(shù)據(jù),僅僅是修改數(shù)據(jù)那么一般使用 Selector。
-
2.2.3. MultiProvider(多個數(shù)據(jù)共享)
在開發(fā)中,我們需要共享的數(shù)據(jù)肯定不止一個,并且數(shù)據(jù)之間我們需要組織到一起,所以一個Provider必然是不夠的。
我們在增加一個新的ChangeNotifier:JKUserViewModelimport 'package:flutter/cupertino.dart'; import 'package:flutterdemo/day10/model/user_model.dart'; class JKUserViewModel with ChangeNotifier { UserModel _userModel; JKUserViewModel(this._userModel); UserModel get userModel => _userModel; set userModel(UserModel value) { _userModel = value; /* 通知所有的監(jiān)聽 */ notifyListeners(); } } class UserModel { /* 名字 */ String name; /* 等級 */ int level; /* 頭像 */ String imageUrl; UserModel(this.name, this.level, this.imageUrl); }
我們之前的使用
void main() { runApp( ChangeNotifierProvider( child: MyApp(), create: (ctx) => JKCounterViewModel(), ), ); }
上面僅僅是共享JKCounterViewModel,那么我們也想共享JKUserViewModel,那么怎么處理呢?
-
方式一:多個Provider之間嵌套,這樣做有很大的弊端,如果嵌套層級過多不方便維護(hù),擴(kuò)展性也比較差
runApp( ChangeNotifierProvider( create: (ctx) => JKUserViewModel(UserModel('wc', 28, 'http://')), child: ChangeNotifierProvider( child: MyApp(), create: (ctx) => JKCounterViewModel(), ), ) );
-
方式二:使用
MultiProvider
(推薦使用)runApp( MultiProvider( child: MyApp(), providers: [ ChangeNotifierProvider(create: (ctx) => JKUserViewModel(UserModel('wc', 28, 'http://'))), ChangeNotifierProvider(create: (ctx) => JKCounterViewModel(),) ], ) ); }
提示:建議創(chuàng)建一個文件去管理共享的數(shù)據(jù),比如創(chuàng)建一個
initalize_providers.dart
的類List<SingleChildWidget> providers = [ ChangeNotifierProvider(create: (ctx) => JKUserViewModel(UserModel('wc', 28, 'http://')),), ChangeNotifierProvider(create: (ctx) => JKCounterViewModel(),) ];
-
在使用的時候我們就可以直接如下
runApp( MultiProvider( child: MyApp(), providers: providers ) );
-
-
-
拓展:在一個Consumer里里使用多個 共享的model,如:Consumer2代表有兩個參數(shù)如下
Consumer2<JKCounterViewModel, JKUserViewModel>( builder: (ctx, counter, user, child){ return Text('當(dāng)前計數(shù):${counter.counter} 名字:${user.userModel.name}', style: TextStyle(fontSize: 30, color: Colors.white),); }, )
提示一:
Consumer2
代表兩個參數(shù),那么builder: (ctx, counter, user, child)
里面也是兩個 共享的model
提示二:Consumer最多到 Consumer6,再多就要 Consumer 嵌套使用了
-