前言
Flutter的很多靈感來(lái)自于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)容都基于此。如果您還未了解過(guò)dart:stream 的話,我建議您先閱讀這篇文章:Dart:什么是Stream。
BLoC
為什么需要狀態(tài)管理
我們一直在找尋強(qiáng)大的狀態(tài)管理方式。也許你并沒(méi)有想過(guò),flutter自身已經(jīng)為我們提供了狀態(tài)管理,而且你經(jīng)常都在用到。
沒(méi)錯(cuò),它就是 Stateful widget。當(dāng)我們接觸到flutter的時(shí)候,首先需要了解的就是有些小部件是有狀態(tài)的,有些則是無(wú)狀態(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è)過(guò)程就是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ì)于性能以及代碼的可閱讀性帶來(lái)一定的影響。
能不能不使用setState就能刷新頁(yè)面呢?如何在多個(gè)頁(yè)面中共享狀態(tài)?我們希望有一種更加強(qiáng)大的方式,來(lái)管理我們的狀態(tài)。
BLoC是什么
BLoC是一種利用reactive programming方式構(gòu)建應(yīng)用的方法,這是一個(gè)由流構(gòu)成的完全異步的世界。

- 用StreamBuilder包裹有狀態(tài)的部件,streambuilder將會(huì)監(jiān)聽一個(gè)流
- 這個(gè)流來(lái)自于BLoC
- 有狀態(tài)小部件中的數(shù)據(jù)來(lái)自于監(jiān)聽的流。
- 用戶交互手勢(shì)被檢測(cè)到,產(chǎn)生了事件。例如按了一下按鈕。
- 調(diào)用bloc的功能來(lái)處理這個(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說(shuō)拜拜??!
BLoC代表業(yè)務(wù)邏輯組件(Business Logic Component),由來(lái)自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è)在不同頁(yè)面使用BLoC共享狀態(tài)信息的app。這兩個(gè)頁(yè)面都依賴于一個(gè)數(shù)字,這個(gè)數(shù)字會(huì)隨著我們按下按鈕的次數(shù)而增加。
第一步:創(chuàng)建BLoC
我們這里的要求很簡(jiǎn)單,僅僅只是輸出一個(gè)數(shù)字而已,然后有一個(gè)方法能夠讓數(shù)字加一。所以我們需要?jiǎng)?chuàng)建一條能夠通過(guò)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í)候假如你的變量沒(méi)有被保護(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è)屏幕中訪問(wèn)同一個(gè)bloc,所以我們只能選擇全局單例模式或者scoped模式。
全局單例模式
全局單例我們只需要在bloc類的文件中創(chuàng)建一個(gè)bloc實(shí)例即可。不過(guò)我并不推薦這種做法,因?yàn)椴恍枰眠@個(gè)bloc的時(shí)候,我們應(yīng)該釋放它。
但是為了讓我解釋的盡量簡(jiǎn)單,后面我將會(huì)基于全局單例模式來(lái)介紹。
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è)“_”占位。
第三步:在頁(yè)面中使用StreamBuilder
這里以第一個(gè)頁(yè)面為例,僅僅顯示文字+數(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è)快照。我們可以通過(guò)snapshot.data訪問(wèn)快照中的數(shù)據(jù)。也可以通過(guò)snapshot.hasError判斷是否有異常,并通過(guò)snapshot.error獲取這個(gè)異常。
- StreamBuilder中的builder是一個(gè)AsyncWidgetBuilder,它能夠異步構(gòu)建widget,當(dāng)檢測(cè)到有數(shù)據(jù)從流中流出時(shí),將會(huì)重新構(gòu)建。
在第二個(gè)頁(yè)面中調(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è)頁(yè)面的數(shù)字無(wú)法顯示,而且控制臺(tái)拋出了這個(gè)異常。
flutter: Bad state: Stream has already been listened to.
這是由于流被重復(fù)監(jiān)聽導(dǎo)致的。 兩個(gè)頁(yè)面中都需要顯示這個(gè)數(shù)字,那么就使用了兩個(gè)StreamBuilder。而StreamBuilder都監(jiān)聽的同一個(gè)流,所以導(dǎo)致了流被重復(fù)監(jiān)聽了。
還記得我們?cè)?a target="_blank">Dart|什么是Stream中說(shuō)的兩種流嗎。沒(méi)錯(cuò),我們創(chuàng)建StreamController的時(shí)候,默認(rèn)是創(chuàng)建的單訂閱流。所以我們需要將流改成廣播流。
_countController = StreamController.broadcast<int>();
只需要在創(chuàng)建StreamController的時(shí)候調(diào)用broadcast方法即可。
來(lái)看看效果
但是我們這里還有一個(gè)小問(wèn)題,你發(fā)現(xiàn)了嗎。
Q&A
為什么第二次進(jìn)入U(xiǎn)nderPage的時(shí)候,計(jì)數(shù)器顯示為0,按了一下才好
這是由于我們?cè)诘谝淮蝡op UnderPage的時(shí)候,這個(gè)頁(yè)面已經(jīng)被銷毀了。當(dāng)我們?cè)賞ush進(jìn)去的時(shí)候,StreamBuilder無(wú)法收聽到最后一次事件(已經(jīng)流過(guò)去了),只能顯示initiaData。而再次點(diǎn)擊時(shí),正確的數(shù)字被add進(jìn)了流,StreamController收聽到了它,所以又能顯示正確的數(shù)據(jù)了。
這個(gè)問(wèn)題能夠解決嗎?
答案是肯定的,使用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ù)面影響。換句話說(shuō):不要過(guò)度設(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)品目錄逐頁(yè)從網(wǎng)絡(luò)中獲取,我們有無(wú)限的這些產(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
來(lái)源:掘金