響應(yīng)式的編程框架中都會有一個永恒的主題——“狀態(tài)(State)管理”,無論是在React/Vue(兩者都是支持響應(yīng)式編程的Web開發(fā)框架)還是Flutter中,他們討論的問題和解決的思想都是一致的。言歸正傳叁执,我們想一個問題款违,StatefulWidget的狀態(tài)應(yīng)該被誰管理?Widget本身隆夯?父Widget?都會别伏?還是另一個對象蹄衷?答案是取決于實際情況!以下是管理狀態(tài)的最常見的方法:
- Widget管理自己的狀態(tài)厘肮。
- Widget管理子Widget狀態(tài)愧口。
- 混合管理(父Widget和子Widget都管理狀態(tài))。
如何決定使用哪種管理方法类茂?下面是官方給出的一些原則可以幫助你做決定:
- 如果狀態(tài)是用戶數(shù)據(jù)耍属,如復(fù)選框的選中狀態(tài)、滑塊的位置巩检,則該狀態(tài)最好由父Widget管理厚骗。
- 如果狀態(tài)是有關(guān)界面外觀效果的,例如顏色兢哭、動畫领舰,那么狀態(tài)最好由Widget本身來管理。
- 如果某一個狀態(tài)是不同Widget共享的則最好由它們共同的父Widget管理。
在Widget內(nèi)部管理狀態(tài)封裝性會好一些冲秽,而在父Widget中管理會比較靈活舍咖。有些時候,如果不確定到底該怎么管理狀態(tài)锉桑,那么推薦的首選是在父widget中管理(靈活會顯得更重要一些)谎仲。
一、狀態(tài)管理的現(xiàn)狀
由于flutter發(fā)展的時間不長刨仑,狀態(tài)管理方案各家也都在探索郑诺,目前主流的狀態(tài)管理,scope_model杉武、redux辙诞、fish_redux、BloC轻抱、rxDart飞涂、provider等等,還有一些探索中的模式祈搜,融合多個模式的優(yōu)點较店,比如reBloc,它們都各具優(yōu)勢容燕,也都不完美梁呈。
目前工程中使用的是fish_redux,使用redux的理念對業(yè)務(wù)層再做了一層封裝蘸秘,對于我們現(xiàn)在的項目來說官卡,太重,學(xué)習(xí)成本也很高醋虏,不利于項目開發(fā)的介入寻咒,再者,現(xiàn)在flutter版本更新頻繁颈嚼,三方的更新速度過慢毛秘,跟不上業(yè)務(wù)的發(fā)展。
flutter的狀態(tài)管理分類
按使用的范圍來分阻课,flutter的狀態(tài)管理分為兩種:局部狀態(tài)和全局狀態(tài)叫挟。
局部狀態(tài):flutter原生提供了InheritWidget控件來實現(xiàn)局部狀態(tài)的控制。當(dāng)InheritedWidget發(fā)生變化時柑肴,它的子樹中所有依賴它數(shù)據(jù)的Widget都會進行rebuild霞揉。但當(dāng)業(yè)務(wù)復(fù)雜時旬薯,邏輯與UI耦合嚴(yán)重晰骑,變的難以維護,復(fù)用性也會非常差。
全局狀態(tài):Flutter沒有提供原生的全局狀態(tài)管理硕舆,基本上是需要依賴第三方庫來實現(xiàn)秽荞。雖然在根控件上使用InheritedWidget也可以實現(xiàn),不過會帶來很多的問題抚官,比如狀態(tài)傳遞過深扬跋,難以維護等。
個人推薦狀態(tài)管理
要應(yīng)對如上的狀態(tài)管理凌节,由于主流方案都各具優(yōu)勢钦听,也都不完美,必然是組合使用倍奢,個人覺得目前最好的方案是RxBloc和provider的組合使用:
- RxBloc在處理大量異步事件以及分離業(yè)務(wù)邏輯上表現(xiàn)很優(yōu)秀朴上,APP業(yè)務(wù)異步事件非常多,復(fù)雜業(yè)務(wù)也很多卒煞,UI和業(yè)務(wù)邏輯分離是家常便飯痪宰,需要優(yōu)秀的設(shè)計來支持,但是在共享狀態(tài)上還有一些缺陷
- Provider是官方團隊推薦的狀態(tài)管理包畔裕,內(nèi)部封裝了InheritedWidget衣撬,RxBloc在共享狀態(tài)上還有一些缺陷由Provider來彌補
- Rx社區(qū)活躍,對Stream做了擴展扮饶,變的更好用功能也更強大具练,Provider是由官方開發(fā),該組合持續(xù)穩(wěn)定
- 局部狀態(tài)使用InheritedWidget實現(xiàn)是沒有問題的甜无,不使用是因為會產(chǎn)生很多的膠水的代碼
Tips:
具體每個方案的優(yōu)劣就不在本文中詳述靠粪,自行g(shù)oogle即可,這里著重介紹RxBloc和Provider的流程和使用毫蚓。
在這之前占键,你需要了解如下概念:
- Dart中的Stream是什么,StreamBuilder是什么元潘,怎么使用
- Rx的基本概念及對Stream封裝后的基本使用
二畔乙、局部狀態(tài)管理 —— RxBLoC
局部狀態(tài)管理,其實flutter自身已經(jīng)為我們提供了狀態(tài)管理翩概,而且你經(jīng)常都在用到牲距,它就是 Stateful widget。當(dāng)我們接觸到flutter的時候钥庇,首先需要了解的就是有些小部件是有狀態(tài)的牍鞠,有些則是無狀態(tài)的。StatelessWidget 與StatefulWidget评姨。
在stateful widget中难述,我們widget的描述信息被放進了State,而stateful widget只是持有一些immutable的數(shù)據(jù)以及創(chuàng)建它的狀態(tài)而已。它的所有成員變量都應(yīng)該是final的胁后,當(dāng)狀態(tài)發(fā)生變化的時候店读,我們需要通知視圖重新繪制,這個過程就是setState攀芯。
這看上去很不錯屯断,我們改變狀態(tài)的時候setState一下就可以了。
在我們一開始構(gòu)建應(yīng)用的時候侣诺,也許很簡單殖演,我們這時候可能并不需要狀態(tài)管理。如下圖年鸳,setState就足夠了剃氧。
但是隨著功能的增加,應(yīng)用程序?qū)袔资畟€甚至上百個狀態(tài)阻星。這個時候應(yīng)用應(yīng)該會是這樣朋鞍。
一旦當(dāng)app的交互變得復(fù)雜,setState出現(xiàn)的次數(shù)便會顯著增加妥箕,每次setState都會重新調(diào)用build方法滥酥,這勢必對于性能、代碼的可讀性和維護性帶來一定的影響畦幢。
那我們就會希望有一種更加強大的方式坎吻,來管理我們的狀態(tài):
- 能不能不使用setState就能刷新頁面呢?
- 頁面足夠復(fù)雜的話宇葱,能否將業(yè)務(wù)和UI分離瘦真,提升可讀性和可維護性?
- 如果頁面足夠復(fù)雜的話黍瞧,能不能盡量少重新調(diào)用子widget的build方法诸尽,提升性能?
- 即使頁面簡單印颤,該方式也能勝任您机,并且不會造成麻煩(比如不像fish_redux有那么多的模板代碼)
于是BLoC呼之欲出,來幫我們處理這些問題年局。
BLoC是什么
BLoC代表業(yè)務(wù)邏輯組件(Business Logic Component)际看,由來自Google的兩位工程師 Paolo Soares和Cong Hui設(shè)計,并在2018年DartConf期間(2018年1月23日至24日)首次展示矢否。有興趣的話可以點擊觀看Youtube視頻
仲闽。
BLoC是一種利用reactive programming方式構(gòu)建應(yīng)用的方法,這是一個由流構(gòu)成的完全異步的世界僵朗。
BLoC工作流程如下:
- 用StreamBuilder包裹有狀態(tài)的部件赖欣,streambuilder將會監(jiān)聽一個流
- 這個流來自于BLoC
- 有狀態(tài)小部件中的數(shù)據(jù)來自于監(jiān)聽的流屑彻。
- 用戶交互手勢被檢測到,產(chǎn)生了事件畏鼓。例如按了一下按鈕酱酬。
- 調(diào)用bloc的功能來處理這個事件
- 在bloc中處理完畢后將會吧最新的數(shù)據(jù)add進流的sink中
- StreamBuilder監(jiān)聽到新的數(shù)據(jù)壶谒,產(chǎn)生一個新的snapshot云矫,并重新調(diào)用build方法
- Widget被重新構(gòu)建
BLoC能夠允許我們完美的分離業(yè)務(wù)邏輯!再也不用考慮什么時候需要刷新了(setState不需要我們顯示調(diào)用)汗菜,一切交給StreamBuilder和BLoC让禀!
Tips:
通過上面的分析,也許我們會說那我們就可以跟StatefulWidget說88了陨界,但通過測試后巡揍,準(zhǔn)確地描述,應(yīng)該是可以和大部分StatefulWidget說88菌瘪,至少保持一個StatefulWidget腮敌,使用其state來保存BLoC實例,Stream在不需要使用的時候俏扩,需要顯示的調(diào)用close方法,不然會造成內(nèi)存泄露或循環(huán)引用。
使用RxDart
ReactiveX是一個強大的庫债鸡,用于通過使用可觀察序列來編寫異步和基于事件的程序亏娜。它突破了語言和平臺的限制,為我們編寫異步程序提供了極大的便利嫉戚。
如果之前接觸過Rx系列刨裆,相信已收獲Rx帶來的便利。
僅使用flutter提供的Stream足夠我們實現(xiàn)BLoC彬檀,但RxDart豐富和擴展了Stream帆啃,使BLoC更簡單更強大。
RxDart對Stream做了哪些封裝窍帝,不是本文的重點链瓦,需要了解的話自行Google,RxDart具體的API到github自行查看RxDart
盯桦。
舉個栗子
我們使用BLoC來實現(xiàn)如下這個功能慈俯,簡單的一個登陸(忽略丑巨的UI,測試而已哈...)拥峦,需求如下:
- 輸入的賬號顯示在頭部已輸入賬號text中
- 賬號在620位贴膘,密碼在612位,符合條件略号,登錄按鈕才可用
- 點擊登錄刑峡,3s后洋闽,修改底部登錄狀態(tài)為已登錄
定義BLoC 抽象類
BLoC中無論是直接使用Stream還是RxDart,本質(zhì)都是Stream突梦,在Stream不需要使用的時候诫舅,我們需要顯示地調(diào)用close方法,所以寫一個簡單的抽象類宫患,所有的BLoC對象都繼承該抽象類刊懈,Stream的close都在dispose方法中實現(xiàn)。
// 所有Bloc的基類
abstract class BlocProviderBase {
// 銷毀stream
void dispose();
}
創(chuàng)建 LoginBLoC 登錄BLoC
/// 登錄 bloc
class LoginBlocProvider extends BlocProviderBase {
String _account = '';
String _password = '';
final PublishSubject<String> _accountSub = PublishSubject<String>();
PublishSubject<String> get accountSub => _accountSub;
final PublishSubject<String> _passwordSub = PublishSubject<String>();
PublishSubject<String> get passwordSub => _passwordSub;
final PublishSubject<bool> _validSub = PublishSubject<bool>();
PublishSubject<bool> get validSub => _validSub;
final PublishSubject<String> _loginSub = PublishSubject<String>();
PublishSubject<String> get loginSub => _loginSub;
// 構(gòu)造方法
LoginBlocProvider() {
_handleSubscript();
}
// 登錄操作
void doLogin() async {
await Future.delayed(Duration(seconds: 3));
print('登錄成功 => 用戶名:$_account, 密碼:$_password');
_loginSub.add('登錄成功~');
}
// 處理訂閱
void _handleSubscript() {
CombineLatestStream<String, bool>([_accountSub, _passwordSub], (values) {
return values.first.length >= 6 &&
values.first.length <= 20 &&
values.last.length >= 6 &&
values.last.length <= 12;
}).listen((value) {
_validSub.sink.add(value);
});
_accountSub.listen((value) {
_account = value;
});
_passwordSub.listen((value) {
_password = value;
});
}
// 銷毀
void dispose() {
_accountSub.close();
_passwordSub.close();
_validSub.close();
_loginSub.close();
}
}
為什么要使用私有變量“_”娃闲,提供get方法
一個應(yīng)用需要大量開發(fā)人員參與虚汛,你寫的代碼也許在幾個月之后被另外一個開發(fā)看到了,這時候假如你的變量沒有被保護的話皇帮,那么是可以隨意改變其中的屬性的卷哩,比如_account,如果直接進行賦值属拾,那么就破壞了整個BLoC的流程将谊。
雖然兩種方式的效果完全一樣,但是第二種方式將會讓我們的business logic零散的混入其他代碼中渐白,提高了代碼耦合程度尊浓,非常不利于代碼的維護以及閱讀,所以為了讓BLoC完全分離我們的業(yè)務(wù)邏輯礼预,請務(wù)必使用私有變量眠砾。
創(chuàng)建 LoginBLoC 實例
flutter常被人詬病的一點是嵌套過深,我們可以通過抽取子widget來一定程度上規(guī)避嵌套地獄托酸,本例中抽取了多個子Widget褒颈,一會詳細(xì)看代碼,但同時也就會帶來一個BLoC實例從父widget傳遞到子widget的問題励堡,這里我們使用Provider來實現(xiàn)局部共享谷丸,不使用InheritWidget的原因,上文中已說明应结,就不贅述了刨疼,Provider的具體使用,后面會詳解鹅龄,這里先主要說明RxBLoC揩慕。
上文中也提到,我們通過Stream和StreamBuilder實現(xiàn)局部刷新扮休,完全不需要使用setState了迎卤,那也就不需要使用StatefulWidget,但是我們需要在頁面銷毀的時候玷坠,調(diào)用BLoC實例的dispose方法蜗搔,我們就至少需要一個頂層的StatefulWidget來保存BLoC實例劲藐。
于是我們在state中創(chuàng)建并保存BLoC實例,并在build的頂層樟凄,使用Provider來共享該實例聘芜,且在state的dispose中調(diào)用BLoC實例中的dispose方法,關(guān)閉Stream:
class _ProviderSharePageHomeState extends State<ProviderSharePageHome> {
LoginBlocProvider _bloc;
@override
void initState() {
super.initState();
_bloc = LoginBlocProvider();
}
@override
Widget build(BuildContext context) {
return Provider(
create: (ctx) => _bloc,
child: Column(
children: <Widget>[
SizedBox(
height: 50,
),
LoginAccountWidget(),
SizedBox(
height: 10,
),
AccountWidget(),
SizedBox(
height: 10,
),
PasswordWidget(),
SizedBox(
height: 10,
),
LoginButtonWidget(),
SizedBox(
height: 10,
),
LoginStateWidget(),
],
),
);
}
@override
void dispose() {
super.dispose();
_bloc.dispose();
}
}
LoginBLoC 的使用
- 輸入流
以賬號輸入為例缝龄,與BLoC的連接如下:
class AccountWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _bloc = Provider.of<LoginBlocProvider>(context);
return TextField(
onChanged: (value) {
_bloc.accountSub.add('$value');
},
decoration: InputDecoration(
labelText: '用戶名',
filled: true,
),
);
}
}
通過對TextField的onChanged方法監(jiān)聽汰现,將新的輸入數(shù)據(jù)通過bloc中的對應(yīng)的stream,發(fā)送給bloc二拐,由bloc做對應(yīng)的邏輯處理服鹅。
- 輸出流
以輸入的用戶名text為例凳兵,使用StreamBuilder構(gòu)建如下:
class LoginAccountWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _bloc = Provider.of<LoginBlocProvider>(context);
return Container(
width: double.infinity,
height: 40,
color: Colors.black12,
child: Center(
child: StreamBuilder(
stream: _bloc.accountSub.where((origin) {
// 丟棄
return origin.length >= 6 && origin.length <= 20;
}).debounceTime(Duration(milliseconds: 500)),
initialData: '',
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return Text(
"輸入的用戶名:${snapshot.data.isEmpty ? '' : snapshot.data}",
style: TextStyle(color: Colors.red),
);
},
),
));
}
}
當(dāng)輸入的賬號和密碼符合規(guī)則百新,登錄按鈕按鈕才會變得可用,同樣是是使用StreamBuilder:
class LoginButtonWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _bloc = Provider.of<LoginBlocProvider>(context);
return Container(
width: 128,
height: 48,
child: StreamBuilder(
stream: _bloc.validSub,
initialData: false,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return FlatButton(
color: Colors.blueAccent,
disabledColor: Colors.blueAccent.withAlpha(50),
child: Text(
'登錄',
style: TextStyle(color: Colors.white),
),
onPressed: snapshot.data
? () {
print('點擊了登錄');
_bloc.doLogin();
}
: null,
);
},
));
}
}
如此庐扫,整個登錄功能就實現(xiàn)了饭望,BLoC的流程就是這樣,其他功能的代碼請詳見DEMO形庭。
大型應(yīng)用中應(yīng)該如何組織 BLoC
大型應(yīng)用程序需要多個BLoC铅辞。一個好的模式是為每個屏幕使用一個頂級組件,并為每個復(fù)雜足夠的小部件使用一個萨醒。但是斟珊,太多的BLoC會變得很麻煩。此外富纸,如果您的應(yīng)用中有數(shù)百個可觀察量(流)囤踩,則會對性能產(chǎn)生負(fù)面影響。換句話說:不要過度設(shè)計你的應(yīng)用程序晓褪。
——Filip Hracek
三堵漱、全局狀態(tài)管理 —— Provider
Provider是目前官方推薦的全局狀態(tài)管理工具,由社區(qū)作者Remi Rousselet 和 Flutter Team共同編寫涣仿。
3.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))的地方
3.1.1 創(chuàng)建自己的ChangeNotifier
我們需要一個ChangeNotifier來保存我們的狀態(tài)好港,所以創(chuàng)建它
- 這里我們可以使用繼承自ChangeNotifier愉镰,也可以使用混入,這取決于概率是否需要繼承自其它的類
- 我們使用一個私有的_counter钧汹,并且提供了getter和setter
- 在setter中我們監(jiān)聽到_counter的改變丈探,就調(diào)用notifyListeners方法,通知所有的Consumer進行更新
class CounterProvider extends ChangeNotifier {
int _counter = 100;
intget counter {
return _counter;
}
set counter(int value) {
_counter = value;
notifyListeners();
}
}
3.1.2 在Widget Tree中插入ChangeNotifierProvider
我們需要在Widget Tree中插入ChangeNotifierProvider崭孤,以便Consumer可以獲取到數(shù)據(jù):
- 將ChangeNotifierProvider放到了頂層类嗤,這樣方便在整個應(yīng)用的任何地方可以使用CounterProvider
void main() {
runApp(ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MyApp(),
));
}
3.1.3 使用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 HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("provider測試"),
),
body: Center(
child: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return Text("當(dāng)前計數(shù):${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
}
),
),
floatingActionButton: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
);
}
}
3.1.4 創(chuàng)建一個新的頁面,在新的頁面中修改數(shù)據(jù)
class BasicProviderSecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二個頁面"),
),
floatingActionButton: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
);
}
}
3.2 Provider詳解
3.2.1 Consumer的builder方法解析
- 參數(shù)一:context笔咽,每個build方法都會有上下文搔预,目的是知道當(dāng)前樹的位置
- 參數(shù)二:ChangeNotifier對應(yīng)的實例,也是我們在builder函數(shù)中主要使用的對象
- 參數(shù)三:child叶组,目的是進行優(yōu)化拯田,如果builder下面有一顆龐大的子樹,當(dāng)模型發(fā)生改變的時候甩十,我們并不希望重新build這顆子樹船庇,那么就可以將這顆子樹放到Consumer的child中,在這里直接引入即可(注意我案例中的Icon所放的位置)
3.2.2 Provider.of解析
事實上侣监,因為Provider是基于InheritedWidget鸭轮,所以我們在使用ChangeNotifier中的數(shù)據(jù)時,我們可以通過Provider.of的方式來使用橄霉,比如下面的代碼:
Text("當(dāng)前計數(shù):${Provider.of<CounterProvider>(context).counter}",
style: TextStyle(fontSize: 30, color: Colors.purple),
),
我們會發(fā)現(xiàn)很明顯上面的代碼會更加簡潔窃爷,那么開發(fā)中是否要選擇上面這種方式呢?
- 答案是否定的姓蜂,更多時候我們還是要選擇Consumer的方式按厘。
為什么呢?因為Consumer在刷新整個Widget樹時覆糟,會盡可能少的rebuild Widget刻剥。
方式一:Provider.of的方式完整的代碼:
- 當(dāng)我們點擊了floatingActionButton時,HomePage的build方法會被重新調(diào)用滩字。
- 這意味著整個HomePage的Widget都需要重新build
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("調(diào)用了HomePage的build方法");
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("當(dāng)前計數(shù):${Provider.of<CounterProvider>(context).counter}",
style: TextStyle(fontSize: 30, color: Colors.purple),
)
],
),
),
floatingActionButton: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
);
}
}
方式二:將Text中的內(nèi)容采用Consumer的方式修改如下:
- 你會發(fā)現(xiàn)HomePage的build方法不會被重新調(diào)用造虏;
- 設(shè)置如果我們有對應(yīng)的child widget,可以采用上面案例中的方式來組織麦箍,性能更高漓藕;
3.2.3 Selector的選擇
Consumer是否是最好的選擇呢?并不是挟裂,它也會存在弊端
- 比如當(dāng)點擊了floatingActionButton時享钞,我們在代碼的兩處分別打印它們的builder是否會重新調(diào)用;
- 我們會發(fā)現(xiàn)只要點擊了floatingActionButton诀蓉,兩個位置都會被重新builder栗竖;
- 但是floatingActionButton的位置有重新build的必要嗎暑脆?沒有,因為它是否在操作數(shù)據(jù)狐肢,并沒有展示添吗;
- 如何可以做到讓它不要重新build了?使用Selector來代替Consumer
直接上代碼:
floatingActionButton: Selector<CounterProvider, CounterProvider>(
selector: (ctx, provider) => provider,
shouldRebuild: (pre, next) => false,
builder: (ctx, counterPro, child) {
print("floatingActionButton展示的位置builder被調(diào)用");
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.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ù)僵腺,你希望如何進行轉(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丧没;
這個時候鹰椒,我們重新測試點擊floatingActionButton锡移,floatingActionButton中的代碼并不會進行rebuild操作呕童。
所以在某些情況下,我們可以使用Selector來代替Consumer淆珊,性能會更高夺饲。
3.2.4 MultiProvider
在開發(fā)中,我們需要共享的數(shù)據(jù)肯定不止一個施符,并且數(shù)據(jù)之間我們需要組織到一起往声,所以一個Provider必然是不夠的。
我們再增加一個新的ChangeNotifier
import'package:flutter/material.dart';
class UserInfo {
String nickname;
int level;
UserInfo(this.nickname, this.level);
}
class UserProvider extends ChangeNotifier {
UserInfo _userInfo = UserInfo("test", 18);
set userInfo(UserInfo info) {
_userInfo = info;
notifyListeners();
}
get userInfo {
return _userInfo;
}
}
如果在開發(fā)中我們有多個Provider需要提供應(yīng)該怎么做呢戳吝?
方式一:多個Provider之間嵌套
- 這樣做有很大的弊端浩销,如果嵌套層級過多不方便維護,擴展性也比較差
runApp(ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: ChangeNotifierProvider(
create: (context) => UserProvider(),
child: MyApp()
),
));
方式二:使用MultiProvider
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (ctx) => CounterProvider()),
ChangeNotifierProvider(create: (ctx) => UserProvider()),
],
child: MyApp(),
));
3.3 RxBLoC+Provider 栗子
由于RxBLoC是使用StreamBuilder來連接BLoC中的Stream听哭,當(dāng)有新數(shù)據(jù)時慢洋,會自動刷新子Widget,在全局共享時陆盘,我們并不需要使用Provider的notify功能普筹,所以共享的數(shù)據(jù)直接使用我們定義好的BLoC就可以了。
由于我們不需要notify功能隘马,所以在APP頂層共享數(shù)據(jù)是太防,也不需要使用ChangeNotifierProvider,直接使用Provider即可酸员,當(dāng)共享多個BLoC時蜒车,使用MultiProvider讳嘱,這個例子即是演示共享多個狀態(tài)。
3.3.1 小需求
需要在全局共享一個count和一個name酿愧,count的初始值是10呢燥,name的初始值是name,在count頁面寓娩,點擊右下角的+叛氨,count累加,在name頁面點擊右下角的+棘伴,name在后面拼接一個1字符串寞埠,count頁面和name頁面,都顯示count+name的格式化字符串焊夸。
3.3.2 創(chuàng)建共享的count和name的BLoC
/// 數(shù)值 bloc
class CounterBlocProvider extends BlocProviderBase {
int _counter = 10;
BehaviorSubject<int> _counterSub = BehaviorSubject.seeded(10);
BehaviorSubject<int> get counterSub => _counterSub;
// 構(gòu)造方法
CounterBlocProvider() {
_handleSubscript();
}
// 增加操作
void doAdd() {
print('執(zhí)行了 counter 增加操作');
_counterSub.add(++_counter);
}
// 處理訂閱
void _handleSubscript() {
_counterSub.listen((value) {
_counter = value;
});
}
// 銷毀
void dispose() {
_counterSub.close();
}
}
/// name bloc
class NameBlocProvider extends BlocProviderBase {
String _name = 'name';
BehaviorSubject<String> _nameSub = BehaviorSubject.seeded('name');
BehaviorSubject<String> get nameSub => _nameSub;
// 構(gòu)造方法
NameBlocProvider() {
_handleSubscript();
}
// 增加操作
void doAdd() {
print('執(zhí)行了 name 增加操作');
_nameSub.add(_name + '1');
}
// 處理訂閱
void _handleSubscript() {
// _nameSub.add(_name);
_nameSub.listen((value) {
_name = value;
});
}
// 銷毀
void dispose() {
_nameSub.close();
}
}
3.3.3 在APP頂層共享全局狀態(tài)
void main() {
runApp(MultiProvider(
providers: [
Provider(create: (ctx) => CounterBlocProvider()),
Provider(create: (ctx) => NameBlocProvider()),
],
child: MyApp(),
));
}
3.3.4 創(chuàng)建count page 和 name page仁连,使用全局共享狀態(tài)
由于我們需要顯示的內(nèi)容是共享的兩個BLoC狀態(tài),所以對兩個Stream進行了合并操作阱穗,使用了RxDart中的CombineLatestStream饭冬,無論是count還是name發(fā)生了變化,在顯示的地方都會實時刷新揪阶。
如果是單純使用Stream昌抠,這個功能實現(xiàn)會比較麻煩,這也是Rx帶來的便利的體現(xiàn)鲁僚。
class ProviderPage extends StatefulWidget {
static const String routeName = "/providerPage";
const ProviderPage({Key key}) : super(key: key);
@override
_ProviderPageState createState() => _ProviderPageState();
}
class _ProviderPageState extends State<ProviderPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('counter provider page'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.people),
onPressed: () {
Navigator.pushNamed(context, ProviderPage2.routeName);
})
],
),
body: Center(
child: Consumer2<CounterBlocProvider, NameBlocProvider>(
builder: (context, cntProvider, nameProvider, child) {
return StreamBuilder(
initialData: '初始化',
stream: CombineLatestStream<dynamic, dynamic>(
[cntProvider.counterSub, nameProvider.nameSub], (values) {
print('合并的值是啥:${values.join(' + ')}');
return values.join(' + ');
}),
builder: (context, snapshot) {
return Chip(label: Text(snapshot.data));
});
}),
),
floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
builder: (context, cntProvider, nameProvider, child) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
cntProvider.doAdd();
});
},
));
}
}
class ProviderPage2 extends StatefulWidget {
static const String routeName = "/providerPage2";
const ProviderPage2({Key key}) : super(key: key);
@override
_ProviderPage2State createState() => _ProviderPage2State();
}
class _ProviderPage2State extends State<ProviderPage2> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('name provider page'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.people),
onPressed: () {
Navigator.pushNamed(context, ProviderPage.routeName);
})
],
),
body: Center(
child: Consumer2<CounterBlocProvider, NameBlocProvider>(
builder: (context, cntProvider, nameProvider, child) {
return StreamBuilder(
initialData: '初始化',
stream: CombineLatestStream<dynamic, dynamic>(
[cntProvider.counterSub, nameProvider.nameSub], (values) {
print('合并的值是啥:${values.join(' + ')}');
return values.join(' + ');
}),
builder: (context, snapshot) {
return Chip(label: Text(snapshot.data));
});
}),
),
floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
builder: (context, cntProvider, nameProvider, child) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
nameProvider.doAdd();
});
},
));
}
}
注意點:
- 這里我們使用Consumer2炊苫,其實就是Consumer的升級,支持2個泛型冰沙,Consumer2侨艾、3...6,以此類推拓挥,同理
- Selector與Consumer一樣唠梨,也有Selector2...6,就是為了支持多個數(shù)據(jù)共享
問題點:
- 由于Selector相對Consumer侥啤,能減少子Widget的build方法調(diào)用次數(shù)当叭,所以能使用Selector當(dāng)然使用Selector,但是這個需求我嘗試了很多次愿棋,只能使用Consumer科展,如果有大佬用Selector能夠?qū)崿F(xiàn),望不吝賜教?酚辍2哦谩!
四、后記
沒有最完美的代碼琅攘,也沒有最完美的框架垮庐,只有適合自己的框架,以上內(nèi)容僅供參考~
上述代碼的DEMO坞琴,傳送門
參考文檔:
https://mp.weixin.qq.com/s/ywGQnaYpioPxlYvYTSpR4w
http://www.reibang.com/p/7573dee97dbb
http://www.reibang.com/p/a5d7758938ef
http://www.reibang.com/p/e0b0169a742e