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

前言

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 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è)過(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)看看效果

image

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

作者:Vadaski
鏈接:https://juejin.im/post/5bb6f344f265da0aa664d68a
來(lái)源:掘金

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容