Flutter | 狀態(tài)管理探索篇——BLoC(三)

前言

Flutter的很多靈感來自于React玻募,它的設(shè)計思想是數(shù)據(jù)與視圖分離蚓再,由數(shù)據(jù)映射渲染視圖滑肉。所以在Flutter中,它的Widget是immutable的摘仅,而它的動態(tài)部分全部放到了狀態(tài)(State)中靶庙。

在之前的文章中,我們已經(jīng)介紹了scoped model與redux兩種狀態(tài)管理方案在flutter中的應(yīng)用娃属。他們似乎都還不錯惶洲,但都還是美中不足。今天我將介紹Google提出的一種全新的解決方案——BLoC膳犹。

在正式開始介紹前恬吕,我希望您已經(jīng)閱讀并理解了stream的相關(guān)知識,后面的內(nèi)容都基于此须床。如果您還未了解過dart:stream 的話铐料,我建議您先閱讀這篇文章:Dart:什么是Stream

BLoC

為什么需要狀態(tài)管理

我們一直在找尋強大的狀態(tài)管理方式豺旬。也許你并沒有想過钠惩,flutter自身已經(jīng)為我們提供了狀態(tài)管理,而且你經(jīng)常都在用到族阅。

沒錯篓跛,它就是 Stateful widget。當我們接觸到flutter的時候坦刀,首先需要了解的就是有些小部件是有狀態(tài)的愧沟,有些則是無狀態(tài)的。stateless widgetstateful widget鲤遥。

在stateful widget中沐寺,我們widget的描述信息被放進了State,而stateful widget只是持有一些immutable的數(shù)據(jù)以及創(chuàng)建它的狀態(tài)而已盖奈。它的所有成員變量都應(yīng)該是final的混坞,當狀態(tài)發(fā)生變化的時候,我們需要通知視圖重新繪制,這個過程就是setState。

這看上去很不錯究孕,我們改變狀態(tài)的時候setState一下就可以了啥酱。
在我們一開始構(gòu)建應(yīng)用的時候,也許很簡單厨诸,我們這時候可能并不需要狀態(tài)管理懈涛。

image

但是隨著功能的增加,你的應(yīng)用程序?qū)袔资畟€甚至上百個狀態(tài)泳猬。這個時候你的應(yīng)用應(yīng)該會是這樣批钠。

image

一旦當app的交互變得復(fù)雜,setState出現(xiàn)的次數(shù)便會顯著增加得封,每次setState都會重新調(diào)用build方法埋心,這勢必對于性能以及代碼的可閱讀性帶來一定的影響。

能不能不使用setState就能刷新頁面呢忙上?如何在多個頁面中共享狀態(tài)拷呆?我們希望有一種更加強大的方式,來管理我們的狀態(tài)疫粥。

BLoC是什么

BLoC是一種利用reactive programming方式構(gòu)建應(yīng)用的方法茬斧,這是一個由流構(gòu)成的完全異步的世界。


image
  • 用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ù)邏輯岁诉!再也不用考慮什么時候需要刷新屏幕了,一切交給StreamBuilder和BLoC!和StatefulWidget說拜拜!跋选!

BLoC代表業(yè)務(wù)邏輯組件(Business Logic Component)涕癣,由來自Google的兩位工程師 Paolo Soares和Cong Hui設(shè)計,并在2018年DartConf期間(2018年1月23日至24日)首次展示前标。點擊觀看Youtube視頻坠韩。

Lets do it候生!

這里我們以一個最簡單的CountApp舉例同眯。簡單介紹BLoC的用法。該項目完整代碼已上傳Github唯鸭。

這是一個在不同頁面使用BLoC共享狀態(tài)信息的app。這兩個頁面都依賴于一個數(shù)字硅确,這個數(shù)字會隨著我們按下按鈕的次數(shù)而增加目溉。


image

第一步:創(chuàng)建BLoC

我們這里的要求很簡單明肮,僅僅只是輸出一個數(shù)字而已,然后有一個方法能夠讓數(shù)字加一缭付。所以我們需要創(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();
 }
}

為什么要使用私有變量“_”

一個應(yīng)用需要大量開發(fā)人員參與,你寫的代碼也許在幾個月之后被另外一個開發(fā)看到了陷猫,這時候假如你的變量沒有被保護的話秫舌,也許同樣是讓count++,他會用countController.sink.add(++_count)這種方法绣檬,而不是調(diào)用 increment方法足陨。

雖然兩種方式的效果完全一樣,但是第二種方式將會讓我們的business logic零散的混入其他代碼中娇未,提高了代碼耦合程度墨缘,非常不利于代碼的維護以及閱讀,所以為了讓BLoC完全分離我們的業(yè)務(wù)邏輯零抬,請務(wù)必使用私有變量镊讼。

第二步:創(chuàng)建BLoC實例

這里有三種方式創(chuàng)建bloc

  • 全局單例創(chuàng)建
  • 局部創(chuàng)建
  • scoped

由于我們需要在兩個屏幕中訪問同一個bloc,所以我們只能選擇全局單例模式或者scoped模式平夜。

全局單例模式

全局單例我們只需要在bloc類的文件中創(chuàng)建一個bloc實例即可蝶棋。不過我并不推薦這種做法,因為不需要用這個bloc的時候忽妒,我們應(yīng)該釋放它嚼松。

但是為了讓我解釋的盡量簡單,后面我將會基于全局單例模式來介紹锰扶。

Scoped模式

創(chuàng)建一個bloc provider類,這里我們需要借助InheritWidget,實現(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;
}

小提示: 這里updateShouldNotify需要傳入一個InheritedWidget oldWidget,但是我們強制返回true坷牛,所以傳一個“_”占位罕偎。

第三步:在頁面中使用StreamBuilder

這里以第一個頁面為例,僅僅顯示文字+數(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ù)代表了這個stream builder監(jiān)聽的流颜及,我們這里監(jiān)聽的是countBloc的value(它是一個stream)。
  • initData代表初始的值蹂楣,因為當這個控件首次渲染的時候俏站,還未與用戶產(chǎn)生交互,也就不會有事件從流中流出痊土。所以需要給首次渲染一個初始值肄扎。
  • builder函數(shù)接收一個位置參數(shù)BuildContext 以及一個snapshot。snapshot就是這個流輸出的數(shù)據(jù)的一個快照。我們可以通過snapshot.data訪問快照中的數(shù)據(jù)犯祠。也可以通過snapshot.hasError判斷是否有異常旭等,并通過snapshot.error獲取這個異常。
  • StreamBuilder中的builder是一個AsyncWidgetBuilder衡载,它能夠異步構(gòu)建widget搔耕,當檢測到有數(shù)據(jù)從流中流出時,將會重新構(gòu)建痰娱。

在第二個頁面中調(diào)用increment

floatingActionButton: FloatingActionButton(
          onPressed: ()=> bloc.increment(),
          child: Icon(Icons.add),
      )

由于這里并不涉及widget的重構(gòu)弃榨,我們只需要調(diào)用bloc的功能即可。

處理廣播流

我們構(gòu)建好ui后梨睁,運行程序?qū)l(fā)現(xiàn)這件奇怪的事鲸睛。


image

第二個頁面的數(shù)字無法顯示,而且控制臺拋出了這個異常而姐。

flutter: Bad state: Stream has already been listened to.

這是由于流被重復(fù)監(jiān)聽導(dǎo)致的腊凶。 兩個頁面中都需要顯示這個數(shù)字,那么就使用了兩個StreamBuilder拴念。而StreamBuilder都監(jiān)聽的同一個流钧萍,所以導(dǎo)致了流被重復(fù)監(jiān)聽了。

還記得我們在Dart|什么是Stream中說的兩種流嗎政鼠。沒錯风瘦,我們創(chuàng)建StreamController的時候,默認是創(chuàng)建的單訂閱流公般。所以我們需要將流改成廣播流万搔。

    _countController = StreamController.broadcast<int>();

只需要在創(chuàng)建StreamController的時候調(diào)用broadcast方法即可。

來看看效果

image

但是我們這里還有一個小問題官帘,你發(fā)現(xiàn)了嗎瞬雹。

Q&A

為什么第二次進入UnderPage的時候,計數(shù)器顯示為0刽虹,按了一下才好

這是由于我們在第一次pop UnderPage的時候酗捌,這個頁面已經(jīng)被銷毀了。當我們再push進去的時候涌哲,StreamBuilder無法收聽到最后一次事件(已經(jīng)流過去了)胖缤,只能顯示initiaData。而再次點擊時阀圾,正確的數(shù)字被add進了流哪廓,StreamController收聽到了它,所以又能顯示正確的數(shù)據(jù)了初烘。

這個問題能夠解決嗎涡真?

答案是肯定的分俯,使用rxdart!rxdart極大的增強了流的功能综膀,解決方法將會在后續(xù)rxdart篇介紹澳迫。

大型應(yīng)用中應(yīng)該如何組織BLoC

大型應(yīng)用程序需要多個BLoC局齿。一個好的模式是為每個屏幕使用一個頂級組件剧劝,并為每個復(fù)雜足夠的小部件使用一個。但是抓歼,太多的BLoC會變得很麻煩讥此。此外,如果您的應(yīng)用中有數(shù)百個可觀察量(流)谣妻,則會對性能產(chǎn)生負面影響萄喳。換句話說:不要過度設(shè)計你的應(yīng)用程序。

——Filip Hracek

一個更加復(fù)雜的app

image

filip提供了一個更復(fù)雜的BLoC樣本蹋半。他將購物應(yīng)用程序重新創(chuàng)建為一個更現(xiàn)實的例子他巨,其中產(chǎn)品目錄逐頁從網(wǎng)絡(luò)中獲取,我們有無限的這些產(chǎn)品列表减江。此外染突,對于目錄中的每個產(chǎn)品,我們希望在產(chǎn)品已在目錄中時稍微更改ProductSquare的顯示辈灼。

了解更多

下面有一些優(yōu)秀的文章能夠給您更多參考

寫在最后

本次所用到的代碼已經(jīng)上傳Github:https://github.com/OpenFlutter/Flutter-Notebook/tree/master/mecury_project/example/bloc_demo

bloc是一個優(yōu)秀的狀態(tài)管理方式份企,它能夠幫助我們更好的構(gòu)建復(fù)雜的大型應(yīng)用。但是他還不是完美的(至少目前不是)巡莹。它在處理大量異步事件以及分離業(yè)務(wù)邏輯上表現(xiàn)很優(yōu)秀司志,但是在共享狀態(tài)上還有一些缺陷。

有人嘗試將redux與bloc結(jié)合使用降宅,試圖找到突破口骂远。這里有一個專門為它編寫的庫:rebloc。感興趣的朋友可以自行了解一下腰根。

如果你在使用bloc進行狀態(tài)管理的時候有任何好的點子激才,或者是疑問,歡迎在下方評論區(qū)以及我的郵箱1652219550a@gmail.com留言唠雕,我會在24小時內(nèi)與您聯(lián)系贸营!

下一篇文章將會為大家介紹Reactive Programming的最佳庫RxDart在BLoC上的實踐,敬請期待岩睁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钞脂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子捕儒,更是在濱河造成了極大的恐慌冰啃,老刑警劉巖邓夕,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異阎毅,居然都是意外死亡焚刚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門扇调,熙熙樓的掌柜王于貴愁眉苦臉地迎上來矿咕,“玉大人,你說我怎么就攤上這事狼钮√贾” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵熬芜,是天一觀的道長莲镣。 經(jīng)常有香客問我,道長涎拉,這世上最難降的妖魔是什么瑞侮? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鼓拧,結(jié)果婚禮上半火,老公的妹妹穿的比我還像新娘。我一直安慰自己毁枯,他們只是感情好慈缔,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著种玛,像睡著了一般藐鹤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赂韵,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天娱节,我揣著相機與錄音,去河邊找鬼祭示。 笑死肄满,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的质涛。 我是一名探鬼主播稠歉,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汇陆!你這毒婦竟也來了怒炸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤毡代,失蹤者是張志新(化名)和其女友劉穎阅羹,沒想到半個月后勺疼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡捏鱼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年执庐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片导梆。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡轨淌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出问潭,到底是詐尸還是另有隱情猿诸,我是刑警寧澤婚被,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布狡忙,位于F島的核電站,受9級特大地震影響址芯,放射性物質(zhì)發(fā)生泄漏灾茁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一谷炸、第九天 我趴在偏房一處隱蔽的房頂上張望北专。 院中可真熱鬧,春花似錦旬陡、人聲如沸拓颓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驶睦。三九已至,卻和暖如春匿醒,著一層夾襖步出監(jiān)牢的瞬間场航,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工廉羔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留溉痢,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓憋他,卻偏偏與公主長得像孩饼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子竹挡,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理镀娶,服務(wù)發(fā)現(xiàn),斷路器此迅,智...
    卡卡羅2017閱讀 134,601評論 18 139
  • 前言 Flutter的很多靈感來自于React汽畴,它的設(shè)計思想是數(shù)據(jù)與視圖分離旧巾,由數(shù)據(jù)映射渲染視圖。所以在Flutt...
    Vadaski閱讀 30,961評論 19 47
  • 壹 暑假我們一家去云南游玩一趟,最喜歡那邊宜人的氣候罢坝,清晨站在窗前廓握,溫柔的風(fēng)輕輕拂過,微涼如水嘁酿,窗外不遠處是煙霧繚...
    玉米嬸閱讀 2,913評論 22 33
  • 他告訴我 他是籠中鳥而我是林中鳥,他渴望和我一樣翱翔在藍天上.于是結(jié)伴而行.很不巧,旅途中出現(xiàn)了意外闹司,當只剩下...
    昕xin閱讀 288評論 0 0
  • 鄰居的孩子要上大學(xué)了娱仔,問我每月要給孩子多少生活費合適?每月給孩子的現(xiàn)在不僅僅是生活費游桩,還包含其他各種消費牲迫。當然,生...
    mimi播報閱讀 301評論 1 5