前言
Flutter的很多靈感來自于React,它的設(shè)計(jì)思想是數(shù)據(jù)與視圖分離,由數(shù)據(jù)映射渲染視圖饵溅。所以在Flutter中,它的Widget是immutable的妇萄,而它的動(dòng)態(tài)部分全部放到了狀態(tài)(State)中蜕企。
在之前的文章中,我們已經(jīng)介紹了scoped model與redux兩種狀態(tài)管理方案在flutter中的應(yīng)用嚣伐。他們似乎都還不錯(cuò)糖赔,但都還是美中不足萍丐。今天我將介紹Google提出的一種全新的解決方案——BLoC轩端。
在正式開始介紹前,我希望您已經(jīng)閱讀并理解了stream的相關(guān)知識(shí)逝变,后面的內(nèi)容都基于此基茵。如果您還未了解過dart:stream 的話奋构,我建議您先閱讀這篇文章:Dart:什么是Stream。
BLoC
為什么需要狀態(tài)管理
我們一直在找尋強(qiáng)大的狀態(tài)管理方式拱层。也許你并沒有想過弥臼,flutter自身已經(jīng)為我們提供了狀態(tài)管理,而且你經(jīng)常都在用到根灯。
沒錯(cuò)径缅,它就是 Stateful widget。當(dāng)我們接觸到flutter的時(shí)候烙肺,首先需要了解的就是有些小部件是有狀態(tài)的纳猪,有些則是無狀態(tài)的。stateless widget 與 stateful widget桃笙。
在stateful widget中氏堤,我們widget的描述信息被放進(jìn)了State,而stateful widget只是持有一些immutable的數(shù)據(jù)以及創(chuàng)建它的狀態(tài)而已搏明。它的所有成員變量都應(yīng)該是final的鼠锈,當(dāng)狀態(tài)發(fā)生變化的時(shí)候,我們需要通知視圖重新繪制,這個(gè)過程就是setState星著。
這看上去很不錯(cuò)购笆,我們改變狀態(tài)的時(shí)候setState一下就可以了。 在我們一開始構(gòu)建應(yīng)用的時(shí)候虚循,也許很簡(jiǎn)單由桌,我們這時(shí)候可能并不需要狀態(tài)管理。
但是隨著功能的增加邮丰,你的應(yīng)用程序?qū)?huì)有幾十個(gè)甚至上百個(gè)狀態(tài)行您。這個(gè)時(shí)候你的應(yīng)用應(yīng)該會(huì)是這樣。
一旦當(dāng)app的交互變得復(fù)雜剪廉,setState出現(xiàn)的次數(shù)便會(huì)顯著增加娃循,每次setState都會(huì)重新調(diào)用build方法,這勢(shì)必對(duì)于性能以及代碼的可閱讀性帶來一定的影響斗蒋。
能不能不使用setState就能刷新頁面呢捌斧?如何在多個(gè)頁面中共享狀態(tài)?我們希望有一種更加強(qiáng)大的方式泉沾,來管理我們的狀態(tài)捞蚂。
BLoC是什么
BLoC是一種利用reactive programming方式構(gòu)建應(yīng)用的方法,這是一個(gè)由流構(gòu)成的完全異步的世界跷究。
- 用StreamBuilder包裹有狀態(tài)的部件姓迅,streambuilder將會(huì)監(jiān)聽一個(gè)流
- 這個(gè)流來自于BLoC
- 有狀態(tài)小部件中的數(shù)據(jù)來自于監(jiān)聽的流。
- 用戶交互手勢(shì)被檢測(cè)到,產(chǎn)生了事件丁存。例如按了一下按鈕肩杈。
- 調(diào)用bloc的功能來處理這個(gè)事件
- 在bloc中處理完畢后將會(huì)吧最新的數(shù)據(jù)add進(jìn)流的sink中
- StreamBuilder監(jiān)聽到新的數(shù)據(jù),產(chǎn)生一個(gè)新的snapshot解寝,并重新調(diào)用build方法
- Widget被重新構(gòu)建
BLoC能夠允許我們完美的分離業(yè)務(wù)邏輯扩然!再也不用考慮什么時(shí)候需要刷新屏幕了,一切交給StreamBuilder和BLoC!和StatefulWidget說拜拜!聋伦!
BLoC代表業(yè)務(wù)邏輯組件(Business Logic Component)夫偶,由來自Google的兩位工程師 Paolo Soares和Cong Hui設(shè)計(jì),并在2018年DartConf期間(2018年1月23日至24日)首次展示觉增。點(diǎn)擊觀看Youtube視頻索守。。
Lets do it抑片!
這里我們以一個(gè)最簡(jiǎn)單的CountApp舉例卵佛。簡(jiǎn)單介紹BLoC的用法。該項(xiàng)目完整代碼已上傳Github敞斋。
這是一個(gè)在不同頁面使用BLoC共享狀態(tài)信息的app截汪。這兩個(gè)頁面都依賴于一個(gè)數(shù)字,這個(gè)數(shù)字會(huì)隨著我們按下按鈕的次數(shù)而增加植捎。
第一步:創(chuàng)建BLoC
我們這里的要求很簡(jiǎn)單衙解,僅僅只是輸出一個(gè)數(shù)字而已,然后有一個(gè)方法能夠讓數(shù)字加一焰枢。所以我們需要?jiǎng)?chuàng)建一條能夠通過int類型數(shù)據(jù)的流蚓峦。
import 'dart:async';
class CountBLoC {
int _count;
StreamController<int> _countController;
CountBLoC() {
_count = 0;
_countController = StreamController<int>();
}
Stream<int> get value => _countController.stream;
increment() {
_countController.sink.add(++_count);
}
dispose() {
_countController.close();
}
}
為什么要使用私有變量“_”
一個(gè)應(yīng)用需要大量開發(fā)人員參與,你寫的代碼也許在幾個(gè)月之后被另外一個(gè)開發(fā)看到了济锄,這時(shí)候假如你的變量沒有被保護(hù)的話暑椰,也許同樣是讓count++,他會(huì)用countController.sink.add(++_count)這種方法荐绝,而不是調(diào)用 increment方法一汽。
雖然兩種方式的效果完全一樣,但是第二種方式將會(huì)讓我們的business logic零散的混入其他代碼中低滩,提高了代碼耦合程度召夹,非常不利于代碼的維護(hù)以及閱讀,所以為了讓BLoC完全分離我們的業(yè)務(wù)邏輯恕沫,請(qǐng)務(wù)必使用私有變量监憎。
第二步:創(chuàng)建BLoC實(shí)例
這里有三種方式創(chuàng)建bloc
- 全局單例創(chuàng)建
- 局部創(chuàng)建
- scoped
由于我們需要在兩個(gè)屏幕中訪問同一個(gè)bloc,所以我們只能選擇全局單例模式或者scoped模式婶溯。
全局單例模式
全局單例我們只需要在bloc類的文件中創(chuàng)建一個(gè)bloc實(shí)例即可鲸阔。不過我并不推薦這種做法偷霉,因?yàn)椴恍枰眠@個(gè)bloc的時(shí)候,我們應(yīng)該釋放它隶债。
但是為了讓我解釋的盡量簡(jiǎn)單,后面我將會(huì)基于全局單例模式來介紹跑筝。
Scoped模式
創(chuàng)建一個(gè)bloc provider類,這里我們需要借助InheritWidget,實(shí)現(xiàn)of方法并讓updateShouldNotify返回true死讹。
class BlocProvider extends InheritedWidget {
CountBLoC bLoC = CountBLoC();
BlocProvider({Key key, Widget child}) : super(key: key, child: child);
@override
bool updateShouldNotify(_) => true;
static CountBLoC of(BuildContext context) =>
(context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bLoC;
}
復(fù)制代碼
小提示: 這里updateShouldNotify需要傳入一個(gè)InheritedWidget oldWidget,但是我們強(qiáng)制返回true曲梗,所以傳一個(gè)“_”占位赞警。
第三步:在頁面中使用StreamBuilder
這里以第一個(gè)頁面為例,僅僅顯示文字+數(shù)字虏两。
StreamBuilder<int>(
stream: bloc.value,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return Text(
'You hit me: ${snapshot.data} times',
style: Theme.of(context).textTheme.display1,
);
})
- StreamBuilder中stream參數(shù)代表了這個(gè)stream builder監(jiān)聽的流愧旦,我們這里監(jiān)聽的是countBloc的value(它是一個(gè)stream)。
- initData代表初始的值定罢,因?yàn)楫?dāng)這個(gè)控件首次渲染的時(shí)候笤虫,還未與用戶產(chǎn)生交互,也就不會(huì)有事件從流中流出祖凫。所以需要給首次渲染一個(gè)初始值琼蚯。
- builder函數(shù)接收一個(gè)位置參數(shù)BuildContext 以及一個(gè)snapshot。snapshot就是這個(gè)流輸出的數(shù)據(jù)的一個(gè)快照惠况。我們可以通過snapshot.data訪問快照中的數(shù)據(jù)遭庶。也可以通過snapshot.hasError判斷是否有異常,并通過snapshot.error獲取這個(gè)異常稠屠。
- StreamBuilder中的builder是一個(gè)AsyncWidgetBuilder峦睡,它能夠異步構(gòu)建widget,當(dāng)檢測(cè)到有數(shù)據(jù)從流中流出時(shí)权埠,將會(huì)重新構(gòu)建榨了。
在第二個(gè)頁面中調(diào)用increment
floatingActionButton: FloatingActionButton(
onPressed: ()=> bloc.increment(),
child: Icon(Icons.add),
)
由于這里并不涉及widget的重構(gòu),我們只需要調(diào)用bloc的功能即可攘蔽。
處理廣播流
我們構(gòu)建好ui后阻逮,運(yùn)行程序?qū)?huì)發(fā)現(xiàn)這件奇怪的事。
第二個(gè)頁面的數(shù)字無法顯示秩彤,而且控制臺(tái)拋出了這個(gè)異常叔扼。
flutter: Bad state: Stream has already been listened to.
這是由于流被重復(fù)監(jiān)聽導(dǎo)致的。 兩個(gè)頁面中都需要顯示這個(gè)數(shù)字漫雷,那么就使用了兩個(gè)StreamBuilder瓜富。而StreamBuilder都監(jiān)聽的同一個(gè)流,所以導(dǎo)致了流被重復(fù)監(jiān)聽了降盹。
還記得我們?cè)?a target="_blank">Dart|什么是Stream中說的兩種流嗎与柑。沒錯(cuò),我們創(chuàng)建StreamController的時(shí)候,默認(rèn)是創(chuàng)建的單訂閱流价捧。所以我們需要將流改成廣播流丑念。
_countController = StreamController.broadcast<int>();
只需要在創(chuàng)建StreamController的時(shí)候調(diào)用broadcast方法即可。
來看看效果
但是我們這里還有一個(gè)小問題结蟋,你發(fā)現(xiàn)了嗎脯倚。
Q&A
為什么第二次進(jìn)入U(xiǎn)nderPage的時(shí)候,計(jì)數(shù)器顯示為0嵌屎,按了一下才好
這是由于我們?cè)诘谝淮蝡op UnderPage的時(shí)候推正,這個(gè)頁面已經(jīng)被銷毀了。當(dāng)我們?cè)賞ush進(jìn)去的時(shí)候宝惰,StreamBuilder無法收聽到最后一次事件(已經(jīng)流過去了)植榕,只能顯示initiaData。而再次點(diǎn)擊時(shí)尼夺,正確的數(shù)字被add進(jìn)了流尊残,StreamController收聽到了它,所以又能顯示正確的數(shù)據(jù)了淤堵。
這個(gè)問題能夠解決嗎夜郁?
答案是肯定的,使用rxdart粘勒!rxdart極大的增強(qiáng)了流的功能竞端,解決方法將會(huì)在后續(xù)rxdart篇介紹。
大型應(yīng)用中應(yīng)該如何組織BLoC
大型應(yīng)用程序需要多個(gè)BLoC庙睡。一個(gè)好的模式是為每個(gè)屏幕使用一個(gè)頂級(jí)組件事富,并為每個(gè)復(fù)雜足夠的小部件使用一個(gè)。但是,太多的BLoC會(huì)變得很麻煩。此外探膊,如果您的應(yīng)用中有數(shù)百個(gè)可觀察量(流)辨液,則會(huì)對(duì)性能產(chǎn)生負(fù)面影響邑狸。換句話說:不要過度設(shè)計(jì)你的應(yīng)用程序。
——Filip Hracek
一個(gè)更加復(fù)雜的app
filip提供了一個(gè)更復(fù)雜的BLoC樣本。他將購(gòu)物應(yīng)用程序重新創(chuàng)建為一個(gè)更現(xiàn)實(shí)的例子,其中產(chǎn)品目錄逐頁從網(wǎng)絡(luò)中獲取贵扰,我們有無限的這些產(chǎn)品列表。此外流部,對(duì)于目錄中的每個(gè)產(chǎn)品戚绕,我們希望在產(chǎn)品已在目錄中時(shí)稍微更改ProductSquare的顯示。
了解更多
下面有一些優(yōu)秀的文章能夠給您更多參考
- [譯]Flutter響應(yīng)式編程:Streams和BLoC
- Build reactive mobile apps in Flutter?—?companion article
- Technical Debt and Streams/BLoC (The Boring Flutter Development Show, Ep. 4)
- Using the BloC Pattern to Build Reactive Applications with Streams in Dart's Flutter Framework
作者:Vadaski
鏈接:https://juejin.im/post/5bb6f344f265da0aa664d68a
來源:掘金