文章結構:
- bloc+rxdart的幾大優點
- 示例程式碼簡單實現呼叫網路介面,然後重新整理頁面顯示
- bloc+rxdart實現原理
- bloc+rxdart經典使用場景,解決了業務場景中的兩個問題。
1.bloc+rxdart的幾大優點
1).實現MVC模式
widget只做UI展示,bloc實現控制邏輯,model做資料封裝。 我在運營線的新需求開發中實際使用了bloc做了一個新的頁面,就是商機敗北頁面。 這個頁面有三級的敗北原因選擇,敗北原因是級聯的,還有每次選擇裝置後要重新請求庫存,提交敗北等操作。
2).實現跨層級model訪問和方法呼叫
在同一個頁面中跨widget資料獲取資料和操作比較多,例如提交敗北的時候,需要獲取多個子widget中的數值,如果沒有使用bloc的話,就需要在子widget的構造方法中傳遞model或回撥方法。
如果頁面層級較多,這種model和回撥方法就需要逐級傳遞。 例如在下圖左邊,需要把submiModel或回撥方法從最上層一直傳遞到最底部,才能做到收集所有的子widget中的資訊,最終集合所有的widget中的引數到submitModel中,提交敗北請求。
下面右邊的程式碼就是這樣逐層傳遞引數的方法,如果層級很多的話會非常頭疼,不僅要重複的宣告變數,而且如果修改的話要每個地方都改。
而使用了bloc的話,建構函式就特別乾淨,不需要逐層傳遞model或者回撥方法。
下面的兩塊程式碼都是頁面widget結構中最下層的兩個widget,不需要在構造方法中傳遞任何引數或回撥方法,但是同樣能呼叫拿到model和呼叫回撥方法。怎麼實現的繼續往下看。:)
3).不直接使用setState方法(StreamController)
之前在專案中直接使用setState會產生一些問題,例如當前State是unmounted狀態,這時直接呼叫setState會拋異常。
而使用bloc的話重新整理頁面是使用如下方式。關鍵的程式碼是_requestController.add
4). 更加簡潔的區域性重新整理(StreamBuilder)
我們現有專案中有一些非常複雜的頁面,但是隻使用了一個檔案,所有的業務邏輯和widget元件都在這個頁面裡面。
下面是一段這種混雜網路請求、widget顯示的程式碼示例。
有多個網路請求和對應的資料顯示widget,不論哪個請求返回或者使用者點選事件,只要呼叫一個setState就能重新整理頁面更新顯示。這樣相比於多個子widget的方式就不用逐層傳遞資料和回撥方法。但是破壞了物件導向開發的基本原則:資訊隱藏,比如修改某個widget的一個小功能,因為在一個很大的檔案中修改,其中業務邏輯錯綜複雜,改一個小功能可能就會影響其他邏輯,產生意料不到的後果,這一塊的程式碼就變得非常難以維護。
同時直接呼叫setState重新整理整個頁面的效能也有問題,也會產生拋異常的問題。
而使用bloc的方式會更加自由,可以像第二點中的圖中一樣使用多個子widget,在子widget中進行重新整理。
也可以直接改造上圖的程式碼,在一個widget中實現區域性重新整理。
第四個優點其實和第三個是一併實現的,都是通過streamBuidler和streamController,這兩者是配套使用的。其實bloc就是streamBuilder+streamController+ancestorWidgetOfExactType,和web端的ajax比較類似,幾項現有技術整合出了新的東西。
StreamBuilder要傳入一個stream物件才能實現重新整理,而這個stream要從streamController中獲取的。
當在上面的程式碼中呼叫_requestController.add(deviceModel)後,下面的StreamBuilder就會自動呼叫builder方法,實現區域性重新整理StreamBuilder,而不是重新整理外層的InkWell。
StreamBuilder是一個Widget,可以在任意地方插入,實現區域性重新整理非常簡單。
2.簡單示例程式碼
要實現一個BlocProvider,現有專案中已經整合,整個專案只需要一個BlocProvider,可以放在Utils目錄中。
實現Bloc其實就只用這一個類和flutter自帶的方法就行了,程式碼非常簡單,並不需要在yaml檔案中引入第三方庫。
所有的bloc都需要繼承BlocBase,通常一個bloc對應一個會重新整理頁面的業務邏輯(如網路請求、切換tab)。
bloc初始化可以在任意父頁面、爺爺頁面中,獲取只需要使用BlocProvider.of(context)。 使用bloc程式碼如下: 有三點需要注意的。 1.bloc要儲存在state中,不然會因為widget重新構建而丟失。 2.bloc需要呼叫dispose,在bloc中的dispose中會呼叫streamController的close方法,最好把bloc的dispose和create在同一個state中呼叫。 3.BlocProvider要在更外面一層建立。下面是bloc建立和blocProvider的程式碼。
3.bloc+rxdart實現原理
1).bloc實現原理
bloc觸發重新整理的方式就是使用StreamBuilder+StreamController,但是直接使用StreamController有一個很大的缺點就是隻能進行一對一的listen,且模式非常單一。所以需要使用到rxdart,下面會講到。
bloc另一重要特性就是跨層級獲取bloc,在bloc可以獲取到model或者呼叫方法。
其中使用了ancestorWidgetOfExactType,因為flutter widget是樹狀結構的,結構層次儲存在BuildContext中,可以從子節點依次往父節點找符合型別的widget,所以所有使用了BlocProvider包裹的widget都可以通過of方法找到,再返回widget中的bloc。(bloc最終是儲存在外一層的state中)。
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
複製程式碼
在下圖中,最下層的widget呼叫of方法後,可以直接獲取到頂層頁面的submitBloc,因為flutter widget是樹狀結構的,結構層次儲存在BuildContext中,可以從子節點依次往父節點找符合型別的widget,所以所有使用了BlocProvider包裹的widget都可以通過of方法找到,再返回widget中的bloc。(bloc最終是儲存在外一層的state中)。
2).rxdart實現原理
StreamController實現了觀察者模式,監聽者不是直接被呼叫,而是處於觀察狀態,當event加入streamController後,監聽者獲得非同步回撥。
rxdart是對StreamController的擴充套件,提供了更多的模式。分為兩個部分Subject和Observable。 其中Observable對stream封裝後提供多個處理方法,例如map、expand、merge、every、contact。var obs = Observable(Stream.fromIterable([1,2,3,4,5]))
.map((item)=>++item);
obs.listen(print);
輸出:2 3 4 5 6
複製程式碼
Subject是對StreamController的擴充套件,常用的有以下幾種。
PublishSubject:StreamController廣播版,streamController只能有一個listener,PublishSubject可以多次listen。下面的其他幾種Subject也都是廣播版。
BehaviorSubject: 快取最近一次的事件。如果先發生event,後listen,也能收到快取的event。
ReplaySubject: 快取所有的事件,之前加入的所有event,listen後,都會順序傳送過來。
4.經典使用場景
1).多個網路圖片元件共用一個http下載。
開發zn_web_image這個網路圖片下載快取元件的時候,為了測試資料載入快取的請求,把圖片連結放在一個陣列中並多次重複。發現同樣的圖片連結在上面已經下載完成後,下面還會重複下載。發現這是由於每次都建立新的bloc導致的,通過使用一個bloc list解決了這個問題。
static ZNImageBLocList _getInstance() {
if (_instance == null) {
_instance = new ZNImageBLocList._internal();
}
return _instance;
}
List<ZNImageBloc> blocList;
ZNImageBloc getBloc(String url,ZNImageConfig config){
for(ZNImageBloc bloc in blocList){
if(bloc.imageUrl==url){
return bloc;
}
}
ZNImageBloc bloc = new ZNImageBloc(url, config);
blocList.add(bloc);
return bloc;
}
複製程式碼
class ZnWebImageState extends State<ZnWebImage> {
ZNImageBloc bloc;
@override
void dispose() {
// TODO: implement dispose
bloc.dispose();
super.dispose();
}
@override
void initState() {
// TODO: implement initState
super.initState();
bloc = ZNImageBLocList.instance.getBloc(widget.url, widget.config);
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return BlocProvider<ZNImageBloc>(
bloc: bloc,
child: ZNImageContainer(),
);
}
}
複製程式碼
這樣多個zn_web_image的widget就能共用同一個bloc,在下載過程中同步顯示進度條,下載完成後同時收到通知顯示下載好的圖片。
2).tabBarView中多個頁面共用一個是否通過認證屬性。
bloc在徵信專案中也解決了一個很頭疼的問題,就是否通過認證的狀態,這個狀態全域性共用,且有多個變更入口,認證狀態變更後要求所有頁面更新顯示。
用到個人認證的地方有多個入口:1.第一個tab中的首頁頂部。
2.確認訂單頁面,可以從第一個tab首頁進入,也可以從第二個tab的訂單列表進入。
3.第四個tab的個人中心頁面。
最開始沒有使用bloc的時候,寫了多個網路請求獲取認證狀態,isAuth屬性也在每個頁面分別儲存。
1.首頁通過requestAuth從伺服器端獲取isAuth屬性。
2.確認訂單頁面也需要從requestAuth獲取isAuth屬性。雖然首頁通過請求獲取到了isAuth屬性,從首頁進入確認訂單頁面可以傳遞isAuth屬性。但是從訂單列表進入時沒有這個屬性,訂單列表是和首頁同時初始化的,還是需要網路請求。
3.個人中心頁面也需要從requestAuth獲取isAuth屬性。雖然首頁通過請求獲取到了isAuth屬性,但是個人中心頁面是和首頁同時在tabbarView中初始化的,不能通過構造方法傳遞isAuth屬性。
第二個問題就是從其中一個頁面進入認證頁面完成認證後,其他所有頁面都需要重新整理認證狀態,沒有使用bloc時,在這些頁面的生命週期方法中寫了重新發起網路請求來重新整理isAuth狀態。這樣非常的麻煩,而且工作量大很多。
使用bloc之後,isAuth這樣全域性共用的屬性,只需要在MaterialApp外面加一層BlocProvider就行了,任何一個子頁面都能直接獲取isAuth,並且能同步isAuth的狀態更新,重新整理UI顯示。
//個人認證bloc
CreditAuthBloc authBloc;
@override
void dispose() {
authBloc.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
authBloc = CreditAuthBloc();
}
@override
Widget build(BuildContext context) {
return BlocProvider<CreditAuthBloc>(bloc: authBloc,child: MaterialApp(
title: '',
initialRoute:'/',
home: Scaffold(
body: Column(
children: <Widget>[
Expanded(
child: TabBarView(physics: NeverScrollableScrollPhysics(),controller: tabController, children: [
ZNMarketPage(),//首頁
ZNOrderList(),//訂單列表
ZNBill(),
ZNMyCenter(),//個人中心
]),
),
],
)),
),);
}
複製程式碼
子頁面獲取bloc
//使用bloc中的auth
// int userAuthStatus = 0; //個人認證 0: 未認證, 1: 已認證
CreditAuthBloc authBloc;
@override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
authBloc = BlocProvider.of<CreditAuthBloc>(context);
}
複製程式碼
通過streamBuilder使用bloc