本文重點分享Flutter中主流狀態(tài)管理庫:BLoC與provider的簡單用法和對比
背景
筆者在今年的惡劣行情下锚贱,終于勇敢的跳槽了碍彭。來到新公司從事自己真心追求的Flutter技術(shù)開發(fā)探越。目前手中的項目是一個完整的Flutter項目朵逝,正在不斷迭代,與我一起共同成長侧蘸。因此,我會逐步把自己的心得記錄下來鹉梨,與君共享讳癌!
前言
作為前端開發(fā)者,MVVM開發(fā)模式應(yīng)該不陌生存皂。MVVM模式是目前前端項目中的主流架構(gòu)晌坤。MVVM — — model、view旦袋、viewModel骤菠。model表示頁面狀態(tài)(即頁面綁定的數(shù)據(jù))、view表示頁面視圖疤孕、viewModel將model和view進行綁定商乎,并且做業(yè)務(wù)邏輯(包括網(wǎng)絡(luò)請求,數(shù)據(jù)更新胰柑、頁面視圖更新驅(qū)動等)的處理截亦。從而實現(xiàn)視圖爬泥、數(shù)據(jù)柬讨、業(yè)務(wù)邏輯完全分離,使得項目結(jié)構(gòu)清晰明朗袍啡,可維護度高踩官。其中,數(shù)據(jù)的改變可以看成頁面狀態(tài)的改變境输,對這些頁面狀態(tài)如何管理的更合理蔗牡,是MVVM架構(gòu)的重中之重。本篇文章最主要也是講解筆者在Flutter中嗅剖,對狀態(tài)管理的使用心得辩越。
Flutter狀態(tài)管理講解
在Flutter中,狀態(tài)管理早已是老生常談的問題了信粮。直到Flutter將provider替代provide作為官方推薦的狀態(tài)管理庫黔攒,F(xiàn)lutter關(guān)于狀態(tài)管理的爭論才開始趨于平靜。
那么狀態(tài)管理為何這么重要呢?這里有一個業(yè)務(wù)場景可以給大家體會下:
假設(shè)服務(wù)器每隔十秒通過websocket給APP推送一次數(shù)據(jù)督惰,數(shù)據(jù)包含文章內(nèi)容不傅,同時也包含閱讀數(shù)、點贊數(shù)等赏胚;APP有兩個頁面访娶,A頁面顯示文章列表,點擊列表項進入B頁面查看文章詳情觉阅。每隔十秒服務(wù)器的消息到達后崖疤,需要實時更新A、B頁面的內(nèi)容典勇。
在Flutter中戳晌,該如何實現(xiàn)?你可能想到在每個頁面注冊一個websocket的接收器痴柔,在各個頁面收到websocket消息通知的時候沦偎,通過setState去更新頁面視圖;撇開性能不講咳蔚,如果有10個頁面豪嚎,就需要定義10個接收器,每個接收器還需要分別處理數(shù)據(jù)然后setState更新視圖谈火。開發(fā)效率上已經(jīng)大打折扣侈询,出錯率極高。
在上面的例子中糯耍,作為前端開發(fā)者肯定會希望只在一個地方接收數(shù)據(jù)扔字,只要數(shù)據(jù)一改變,視圖就實時更新温技,無需每個頁面都多setState操作革为。我們把數(shù)據(jù)接收器定義成更新事件的發(fā)布者,每個頁面都是一個監(jiān)聽者舵鳞。發(fā)布者發(fā)出事件后震檩,監(jiān)聽者馬上可以收到,視圖馬上更新蜓堕。其實這就是典型的發(fā)布訂閱模式抛虏。顯而易見,大部分前端包括Flutter中的狀態(tài)管理套才,都是基于發(fā)布訂閱的設(shè)計模式出發(fā)出發(fā)的迂猴。
Flutter中的發(fā)布訂閱模式,可以直接使用stream流機制背伴。(關(guān)于stream的系統(tǒng)學(xué)習(xí)請見:https://juejin.im/post/6844904163407577096)沸毁。
以上面的例子儡率,我們需要一個websocket接收器,接收消息后通過streamController.skin.add發(fā)布事件以清,頁面中會注冊監(jiān)聽器:streamController.stream.listen儿普,在監(jiān)聽回調(diào)中實現(xiàn)setState去更新視圖。
事實上掷倔,F(xiàn)lutter目前已有的狀態(tài)管理眉孩,如rxdart、BLoC勒葱、fluter_redux浪汪、provider等,都離不開對stream流進行封裝凛虽,再加入widget的封裝演化出StreamBuilder死遭、BlocBuilder等布局組件,從而達到無需setState就能實時更新視圖的效果凯旋。(關(guān)于Flutter狀態(tài)管理的演變過程呀潭,可見:https://juejin.im/post/6844904035439345671)
進入主題
很感謝大家讀到這里,前面花費了大量文字解釋并代入Flutter的狀態(tài)管理至非,無非就是為了說明狀態(tài)管理是Flutter項目中不可獲取的一部分钠署。這篇文章重點要講的,就是如何使用荒椭、如何選用Flutter狀態(tài)管理庫谐鼎。目前一般公司最常用的狀態(tài)管理庫是:BLoC和Provider。接下來重點講解兩者的使用和比較趣惠。
BLoC
目錄結(jié)構(gòu)
BLoC是谷歌提出的一種設(shè)計模式狸棍,利用流的方式實現(xiàn)界面的異步渲染和重繪,我們可以非澄肚模快速的通過BLoC實現(xiàn)業(yè)務(wù)與界面的分離草戈。一般情況下,我們會在項目中引入flutter_bloc這個庫傍菇。一個BLoC管理猾瘸,有三個文件:bloc、event丢习、state;
使用方法
當(dāng)一個組件需要使用到BLoC狀態(tài)管理時淮悼,需要在調(diào)用組件之前咐低,需要聲明下BLoC的提供者,具體寫法如下:
BlocProvider<BadgesBloc>(
????create: (context) =>BadgesBloc(),
????child: UserPage()
)
當(dāng)一個頁面有多個BLoC提供者袜腥,或者整個app有幾個通用的BLoC提供者见擦,有多個頁面都需要使用钉汗,即可提前在加載app之前全局聲明±鹇牛可以使用MultiBlocProvider進行聲明损痰,具體寫法如下:
MultiBlocProvider(
? providers: [
????BlocProvider<BadgesBloc>(create:(context) => BadgesBloc()),
????BlocProvider(create: (context) =>XXX()),
],
????child: MaterialApp()
)
使用中,頁面布局將使用BlocBuilder創(chuàng)建widget酒来,用戶在頁面中通過BlocProvider.of(context).add()發(fā)起事件卢未,
///? 布局示例
BlocBuilder<BadgesBloc, BadgesState>(
????// 接收bloc返回的state,視圖與state中的變量進行綁定
????builder: (context, state) {
????var isShowBadge = false;
????if (state is BadgesInitialState) {
????????isShowBadge = state.unReadNotification;
????}
????return Badge(
????????showBadge: isShowBadge,
????????shape: BadgeShape.circle,
????????position: BadgePosition(top: -3, right: -3),
????????child: Icon( Icons.notifications_none, color: Color(0xFFFFFFFF),
????????),
????);
})
/// 頁面發(fā)起事件
// 發(fā)出的重設(shè)Badge的事件堰汉,事件要求傳參為bool
BlocProvider.of<BadgesBloc>(context).add(ResetBadgeEvent(true));?
此時在bloc中就會接收到事件辽社,判斷發(fā)起的事件是event中的哪個事件,然后返回對應(yīng)的state翘鸭。
@override
Stream<BadgesState> mapEventToState(BadgesEvent event) async* {
????if (event is ResetBadgeEvent) {
????????yield BadgesInitialState(event.unReadNotification);?
????}?
}
Stream<BadgesInitialState> _mapGetActivityCountState(isShow) async* {
? ? // 此處更改狀態(tài)的值滴铅,讓上面的視圖代碼可以根據(jù)此值進行更新
? ? ?yield BadgesInitialState(isShow);
}
同時需要補上event、state的代碼截圖
使用心得
不難得出就乓,BLoC使用起來結(jié)構(gòu)相對復(fù)雜汉匙,每一個狀態(tài)則需要三個文件。而且發(fā)出事件時生蚁,需要用到context盹兢。即常理下,只能在build生命周期內(nèi)發(fā)出事件守伸。若是上述案例中的websocket監(jiān)聽中要發(fā)出事件绎秒,將會顯得困難,需要使用全局的context(全局的context可使用GlobalKey<NavigatorState> )尼摹。這一點勢必會成為BLoC的一個很重大的弊端(后面筆者會有詳細(xì)解釋)见芹。
Provider
Provider是Flutter官方自己維護的,也是官方最為推薦的狀態(tài)管理庫蠢涝,它的特點是:不復(fù)雜玄呛、好理解,可控度高和二。我們會在項目中引入provider這個庫徘铝,Provider沒有像BLoC那樣負(fù)責(zé)的目錄結(jié)構(gòu),下面直接講解用法惯吕。
使用方法
當(dāng)一個組件需要使用到Provider狀態(tài)管理時惕它,需要在調(diào)用組件之前,需要聲明下Provider的提供者废登,具體寫法如下:
ChangeNotifierProvider<LoginViewModel>.value(
????notifier: LoginViewModel(), ????
????child:LoginPage(),
)
當(dāng)一個頁面有多個Provider提供者淹魄,或者整個app有幾個通用的Provider提供者,有多個頁面都需要使用堡距,即可提前在加載app之前全局聲明甲锡≌捉叮可以使用MultiProvider進行聲明,具體寫法如下:
MultiProvider(
????providers: [
? ??????ChangeNotifierProvider<LoginViewModel>( create: (_) => LoginViewModel(),),
? ??????ChangeNotifierProvider<HomeViewModel>( create: (_) =>?HomeViewModel(),),
????],
????child: MaterialApp()
)
使用中缤沦,頁面布局需在build中創(chuàng)建一個provider對象虎韵,之后直接在widget中綁定viewModel中的數(shù)據(jù)或者觸發(fā)事件即可
/// 創(chuàng)建provider對象
var loginVM = Provider.of<LoginViewModel>(context);
Column(
????children: <Widget>[
????????new Padding(
????????????padding: EdgeInsets.only(top: 85),
????????????child: new Container(
????????????????height: 85.h, width: 486.w,
????????????????child: TextFormField(
? ??????????????????// 綁定viewModel的數(shù)據(jù)
????????????????????controller: loginVM.userNameController,
????????????????????decoration: InputDecoration(
????????????????????????hintText: "請輸入用戶名",
????????????????????????icon: Icon(Icons.person),
????????????????????????hintStyle: TextStyle(color: Colors.grey, fontSize: 24.sp),
????????????),
????????????validator: (value) {
????????????????return value.trim().length > 0 ? null : "必填選項"; }
????????)
????)
),
????????new Padding(
????????????padding: EdgeInsets.only(top: 40),
????????????child: new Container(
????????????????height: 90.h, width: 486.w,
????????????????child: new RaisedButton(
? ? ? ? ? ? ? ? ? ? ? ?// 點擊觸發(fā)viewModel中的方法
? ? ? ? ? ? ? ? ? ? ? onPressed: () { loginVM.loginHandel(context)},
????????????color: const Color(0xff00b4ed), shape: StadiumBorder(),
????????????child: new Text( "登錄",
????????????????????style: new TextStyle(color: Colors.white, fontSize: 32.sp),
????????????),
????????),
????)
)]
我們來看看viewModel中的寫法,class必須繼承ChangeNotifier缸废,當(dāng)有數(shù)據(jù)需要更新的時候包蓝,調(diào)用notifyListeners(),頁面就會刷新呆奕。
使用心得
Provider確實使用起來方便很多养晋。在更新時,無需使用頁面手動觸發(fā)梁钾,也就是無需過多的跟context打交道绳泉,可以避免BLoC使用全局key的弊端。最重要的一點是姆泻,provider非常符合我們對mvvm的一般認(rèn)識零酪,在build中聲明provider后,直接就可以調(diào)用provider中的值進行視圖渲染拇勃。
對比分析
上面簡單說明BLoC和Provider兩種最常用的狀態(tài)管理框架的用法四苇。接下來將是這篇文章的重中之重,分析兩者的區(qū)別:
一方咆、理解難易程度
BLoC是一種設(shè)計模式月腋,主要思想是基于Dart Stream流的發(fā)布訂閱者模式。使用者需要對Stream有較為深刻的理解瓣赂。另外榆骚,在使用上與傳統(tǒng)的MVVM模式還是有所差別的。相比之下煌集,Provider就更容易理解妓肢,只需在頁面中聲明一次Provider,即可直接在視圖中引用數(shù)據(jù)苫纤,理解起來特別容易碉钠。?
二、使用限制
BLoC對上下文context的依賴要更強卷拘,BLoC中發(fā)布者通過.add發(fā)布事件喊废,但發(fā)布時必須攜帶上下文context,視圖中通過BlocBuilder作為訂閱者恭金,接收到state改變時操禀,rebuild布局。相比之下横腿,Provider的viewModel只需要繼承ChangeNotifier颓屑,更新時直接notifyListeners(),隨時更新耿焊,無需綁定上下文揪惦。這一點個人認(rèn)為是provider很大的一個優(yōu)勢。
因為MVVM模式在應(yīng)用中罗侯,無非就是解決視圖和數(shù)據(jù)業(yè)務(wù)層的耦合器腋。如果在viewModel層還需要依賴context,那么在業(yè)務(wù)邏輯負(fù)責(zé)的情況下钩杰,勢必會更加麻煩纫塌。
三、易用程度
這個就無需多說了讲弄,Provider完勝BLoC措左。創(chuàng)建一個BLoC狀態(tài)管理,需要創(chuàng)建State避除、Event怎披、Bloc三個文件去管理,Provider一個搞定瓶摆。
四凉逛、 顆粒度掌控
顆粒度即局部刷新的能力,其實這也是狀態(tài)管理中最頭疼的問題群井。雖然Flutter高效的渲染機制状飞,讓我們可以忽略刷新帶來的內(nèi)存問題。
但是追求極致的程序員們书斜,就算我的頁面有100個數(shù)據(jù)诬辈,綁定在100個組件上;我改變了一個數(shù)據(jù)菩佑,我就只想頁面刷新這一個組件自晰,其他99個不動。
因此就引出一個顆粒度的掌控能力稍坯。BLoC中酬荞,只要是BlocBuilder包裹下的組件,當(dāng)State中的任一數(shù)據(jù)改變瞧哟,整個組件都會rebuild混巧,這是必然的。為了控制更小的局部刷新勤揩,只能不斷拆分更細(xì)的BLoC咧党;
同樣,Provider要做到局部刷新陨亡,可以使用Consumer控件包裹傍衡,(這里跟BlocBuilder是一個原理)深员,也需要拆分更多的viewModel層。在局部刷新這個問題上蛙埂,其實都是一樣不分伯仲倦畅,如果一定要比個高低,我覺得BLoC的做法更加直觀绣的,反正我BlocBuilder包裹的夠細(xì)叠赐,就越能精致顆粒度。
總結(jié)
綜上屡江,業(yè)界以及個人其實都會比較偏向使用provider作為狀態(tài)管理的首選庫芭概。但BLoC雖然用起來比較負(fù)責(zé),但不可否認(rèn)其在大型項目確實也挺可控惩嘉;
其實不妨可以結(jié)合使用罢洲,筆者目前的項目就是結(jié)合使用Provider和BLoC,效果也還不錯宏怔。遇到狀態(tài)復(fù)雜的奏路,就偷偷懶用Provider;一般頁面下臊诊,就使用BLoC以便更好的把控顆粒度鸽粉。倒也用的挺香,
總之抓艳,選擇哪種狀態(tài)管理沒有特定的規(guī)律触机,上手時間、可維護性玷或、開發(fā)成本都是需要考慮的主觀因素儡首,還是要視團隊和具體應(yīng)用場景而定。如果業(yè)務(wù)場景只是一個輸入框和按鈕偏友,過度的模式設(shè)計其實就會顯得畫蛇添足蔬胯。需要大家理性選擇。
希望大家多多指導(dǎo)我位他!