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

前言

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 widgetstateful 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方法即可。

來看看效果

image

但是我們這里還有一個(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)秀的文章能夠給您更多參考

作者:Vadaski
鏈接:https://juejin.im/post/5bb6f344f265da0aa664d68a
來源:掘金

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末枝冀,一起剝皮案震驚了整個(gè)濱河市舞丛,隨后出現(xiàn)的幾起案子耘子,更是在濱河造成了極大的恐慌,老刑警劉巖球切,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谷誓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡吨凑,警方通過查閱死者的電腦和手機(jī)捍歪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怀骤,“玉大人费封,你說我怎么就攤上這事焕妙〗祝” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵焚鹊,是天一觀的道長(zhǎng)痕届。 經(jīng)常有香客問我,道長(zhǎng)末患,這世上最難降的妖魔是什么研叫? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮璧针,結(jié)果婚禮上嚷炉,老公的妹妹穿的比我還像新娘。我一直安慰自己探橱,他們只是感情好申屹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著隧膏,像睡著了一般哗讥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胞枕,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天杆煞,我揣著相機(jī)與錄音,去河邊找鬼腐泻。 笑死决乎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的派桩。 我是一名探鬼主播瑞驱,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼窄坦!你這毒婦竟也來了唤反?” 一聲冷哼從身側(cè)響起凳寺,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎彤侍,沒想到半個(gè)月后肠缨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盏阶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年晒奕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片名斟。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脑慧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出砰盐,到底是詐尸還是另有隱情闷袒,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布岩梳,位于F島的核電站囊骤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏冀值。R本人自食惡果不足惜也物,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望列疗。 院中可真熱鬧滑蚯,春花似錦、人聲如沸抵栈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竭讳。三九已至创葡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绢慢,已是汗流浹背灿渴。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胰舆,地道東北人骚露。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缚窿,于是被迫代替她去往敵國(guó)和親棘幸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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