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

Vadaski發表於2018-10-10

前言

Flutter的很多靈感來自於React,它的設計思想是資料與檢視分離,由資料對映渲染檢視。所以在Flutter中,它的Widget是immutable的,而它的動態部分全部放到了狀態(State)中。

在之前的文章中,我們已經介紹了scoped model與redux兩種狀態管理方案在flutter中的應用。他們似乎都還不錯,但都還是美中不足。今天我將介紹Google提出的一種全新的解決方案——BLoC。

在正式開始介紹前,我希望您已經閱讀並理解了stream的相關知識,後面的內容都基於此。如果您還未了解過dart:stream 的話,我建議您先閱讀這篇文章:Dart:什麼是Stream

BLoC

為什麼需要狀態管理

我們一直在找尋強大的狀態管理方式。也許你並沒有想過,flutter自身已經為我們提供了狀態管理,而且你經常都在用到。

沒錯,它就是 Stateful widget。當我們接觸到flutter的時候,首先需要了解的就是有些小部件是有狀態的,有些則是無狀態的。stateless widgetstateful widget

在stateful widget中,我們widget的描述資訊被放進了State,而stateful widget只是持有一些immutable的資料以及建立它的狀態而已。它的所有成員變數都應該是final的,當狀態發生變化的時候,我們需要通知檢視重新繪製,這個過程就是setState。

這看上去很不錯,我們改變狀態的時候setState一下就可以了。 在我們一開始構建應用的時候,也許很簡單,我們這時候可能並不需要狀態管理。

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

但是隨著功能的增加,你的應用程式將會有幾十個甚至上百個狀態。這個時候你的應用應該會是這樣。

Flutter | 狀態管理探索篇——BLoC(三)
一旦當app的互動變得複雜,setState出現的次數便會顯著增加,每次setState都會重新呼叫build方法,這勢必對於效能以及程式碼的可閱讀性帶來一定的影響。

能不能不使用setState就能重新整理頁面呢?如何在多個頁面中共享狀態?我們希望有一種更加強大的方式,來管理我們的狀態。

BLoC是什麼

BLoC是一種利用reactive programming方式構建應用的方法,這是一個由流構成的完全非同步的世界。

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

  • 用StreamBuilder包裹有狀態的部件,streambuilder將會監聽一個流
  • 這個流來自於BLoC
  • 有狀態小部件中的資料來自於監聽的流。
  • 使用者互動手勢被檢測到,產生了事件。例如按了一下按鈕。
  • 呼叫bloc的功能來處理這個事件
  • 在bloc中處理完畢後將會吧最新的資料add進流的sink中
  • StreamBuilder監聽到新的資料,產生一個新的snapshot,並重新呼叫build方法
  • Widget被重新構建

BLoC能夠允許我們完美的分離業務邏輯!再也不用考慮什麼時候需要重新整理螢幕了,一切交給StreamBuilder和BLoC!和StatefulWidget說拜拜!!

BLoC代表業務邏輯元件(Business Logic Component),由來自Google的兩位工程師 Paolo Soares和Cong Hui設計,並在2018年DartConf期間(2018年1月23日至24日)首次展示。點選觀看Youtube視訊。

Lets do it!

這裡我們以一個最簡單的CountApp舉例。簡單介紹BLoC的用法。該專案完整程式碼已上傳Github

這是一個在不同頁面使用BLoC共享狀態資訊的app。這兩個頁面都依賴於一個數字,這個數字會隨著我們按下按鈕的次數而增加。

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

第一步:建立BLoC

我們這裡的要求很簡單,僅僅只是輸出一個數字而已,然後有一個方法能夠讓數字加一。所以我們需要建立一條能夠通過int型別資料的流。

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();
 }
}
複製程式碼

為什麼要使用私有變數“_”

一個應用需要大量開發人員參與,你寫的程式碼也許在幾個月之後被另外一個開發看到了,這時候假如你的變數沒有被保護的話,也許同樣是讓count++,他會用countController.sink.add(++_count)這種方法,而不是呼叫 increment方法。

雖然兩種方式的效果完全一樣,但是第二種方式將會讓我們的business logic零散的混入其他程式碼中,提高了程式碼耦合程度,非常不利於程式碼的維護以及閱讀,所以為了讓BLoC完全分離我們的業務邏輯,請務必使用私有變數。

第二步:建立BLoC例項

這裡有三種方式建立bloc

  • 全域性單例建立
  • 區域性建立
  • scoped

由於我們需要在兩個螢幕中訪問同一個bloc,所以我們只能選擇全域性單例模式或者scoped模式。

全域性單例模式

全域性單例我們只需要在bloc類的檔案中建立一個bloc例項即可。不過我並不推薦這種做法,因為不需要用這個bloc的時候,我們應該釋放它。

但是為了讓我解釋的儘量簡單,後面我將會基於全域性單例模式來介紹。

Scoped模式

建立一個bloc provider類,這裡我們需要藉助InheritWidget,實現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

這裡以第一個頁面為例,僅僅顯示文字+數字。

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引數代表了這個stream builder監聽的流,我們這裡監聽的是countBloc的value(它是一個stream)。
  • initData代表初始的值,因為當這個控制元件首次渲染的時候,還未與使用者產生互動,也就不會有事件從流中流出。所以需要給首次渲染一個初始值。
  • builder函式接收一個位置引數BuildContext 以及一個snapshot。snapshot就是這個流輸出的資料的一個快照。我們可以通過snapshot.data訪問快照中的資料。也可以通過snapshot.hasError判斷是否有異常,並通過snapshot.error獲取這個異常。
  • StreamBuilder中的builder是一個AsyncWidgetBuilder,它能夠非同步構建widget,當檢測到有資料從流中流出時,將會重新構建。

在第二個頁面中呼叫increment

floatingActionButton: FloatingActionButton(
          onPressed: ()=> bloc.increment(),
          child: Icon(Icons.add),
      )
複製程式碼

由於這裡並不涉及widget的重構,我們只需要呼叫bloc的功能即可。

處理廣播流

我們構建好ui後,執行程式將會發現這件奇怪的事。

Flutter | 狀態管理探索篇——BLoC(三)
第二個頁面的數字無法顯示,而且控制檯丟擲了這個異常。

flutter: Bad state: Stream has already been listened to.
複製程式碼

這是由於流被重複監聽導致的。 兩個頁面中都需要顯示這個數字,那麼就使用了兩個StreamBuilder。而StreamBuilder都監聽的同一個流,所以導致了流被重複監聽了。

還記得我們在Dart|什麼是Stream中說的兩種流嗎。沒錯,我們建立StreamController的時候,預設是建立的單訂閱流。所以我們需要將流改成廣播流。

    _countController = StreamController.broadcast<int>();
複製程式碼

只需要在建立StreamController的時候呼叫broadcast方法即可。

來看看效果

Flutter | 狀態管理探索篇——BLoC(三)
但是我們這裡還有一個小問題,你發現了嗎

Q&A

為什麼第二次進入UnderPage的時候,計數器顯示為0,按了一下才好

這是由於我們在第一次pop UnderPage的時候,這個頁面已經被銷燬了。當我們再push進去的時候,StreamBuilder無法收聽到最後一次事件(已經流過去了),只能顯示initiaData。而再次點選時,正確的數字被add進了流,StreamController收聽到了它,所以又能顯示正確的資料了。

這個問題能夠解決嗎?

答案是肯定的,使用rxdart!rxdart極大的增強了流的功能,解決方法將會在後續rxdart篇介紹。

大型應用中應該如何組織BLoC

大型應用程式需要多個BLoC。一個好的模式是為每個螢幕使用一個頂級元件,併為每個複雜足夠的小部件使用一個。但是,太多的BLoC會變得很麻煩。此外,如果您的應用中有數百個可觀察量(流),則會對效能產生負面影響。換句話說:不要過度設計你的應用程式。

——Filip Hracek

一個更加複雜的app

Flutter | 狀態管理探索篇——BLoC(三)
filip提供了一個更復雜的BLoC樣本。他將購物應用程式重新建立為一個更現實的例子,其中產品目錄逐頁從網路中獲取,我們有無限的這些產品列表。此外,對於目錄中的每個產品,我們希望在產品已在目錄中時稍微更改ProductSquare的顯示。

瞭解更多

下面有一些優秀的文章能夠給您更多參考

寫在最後

本次所用到的程式碼已經上傳Github:github.com/OpenFlutter…

bloc是一個優秀的狀態管理方式,它能夠幫助我們更好的構建複雜的大型應用。但是他還不是完美的(至少目前不是)。它在處理大量非同步事件以及分離業務邏輯上表現很優秀,但是在共享狀態上還有一些缺陷。

有人嘗試將redux與bloc結合使用,試圖找到突破口。這裡有一個專門為它編寫的庫:rebloc。感興趣的朋友可以自行了解一下。

如果你在使用bloc進行狀態管理的時候有任何好的點子,或者是疑問,歡迎在下方評論區以及我的郵箱1652219550a@gmail.com留言,我會在24小時內與您聯絡!

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

相關文章