[Flutter Package]狀態管理之BLoC的封裝

Realank Liu發表於2018-11-08

一、狀態管理難題

在Flutter中有一個很有爭論的部分——揪淨該怎樣狀態管理

我一開始堅持移動端的思路,使用MVC或者MVP等,還有的同學支援前端思路,使用Redux或者RxDart等,也有人說Google推薦使用BLoC,還有像我翻譯的一篇文章一樣:[譯]讓我來幫你理解和選擇Flutter狀態管理方案, 使用Redux和BLoC的混合。

  • MVC和MPV中狀態和重新整理是分離的
  • Redux把所有狀態存到一起有些臃腫,也會存在UI不必要的重新整理,這在App上需要儘量避免
  • BLoC是使用Stream或者RxDart等工具,將資料(狀態)獨立出去,然後當狀態有更新的時候,資料使用者自動更新

我一直在想,能不能儘量使用在iOS上開發的方式來做,後來發現行不通,原因是flutter的渲染方式和移動端完全不同,它採用的React的思路。

移動端的UI控制元件可以通過修改其屬性改變外觀,但是flutter和RN,改變樣式基本是靠重新渲染,所以想要更新內容,就要改變state,然後再通過setState()更新UI。

所以flutter裡更新UI,先天是割裂的,這一點和React一樣,所以就需要觀察者(或是什麼類似的)設計模式的封裝,來降低更新UI的複雜程度,減少耦合或者過多的狀態宣告。

二、BLoC介紹

說了這麼多,其實就是想說,BLoC還是值得一試的,他能解決狀態和UI揉雜在一起的問題,也沒有Redux這麼重,適合用於簡單業務場景的資料同步。

BLoC是一種設計模式,官方並沒有給出封裝的程式碼,網上搜到的程式碼大同小異(不知道是不是Google給的最佳實踐),但是他們的共同特點就是,初始值賦值上有問題,UI的初始值,沒有用BLoC的資料部分給出的初始值,只是恰好兩者值相等,那當需要改變初始值的時候,就需要改很多處,這顯然不可接受。(原因這裡就不再贅述了)

剛剛也說了,BLoC的實現,使用了Stream或者RxDart,我傾向於使用Stream,因為它是內建的庫,RxDart功能我沒有評估,但是顯然針對簡單業務場景,過於重了。

關於BLoC的實現細節,這裡推薦大家看鑫磊的文章:Flutter | 狀態管理探索篇——BLoC(三),我在這裡不詳細說

BLoC的結構如下:

[Flutter Package]狀態管理之BLoC的封裝

我也不知道這應該歸類為觀察者模式,還是生產者消費者模式,總之是:

  • BLoC資料模組,持有一個StreamController,來管理stream

  • UI需要展示資料的地方,使用StreamBuilder來監聽stream變化

  • 當stream裡的資料變化時,就會自動重新整理子UI。

  • 需要更新資料時,比如點選了某個按鈕,則會操作BLoC資料模組,通過其StreamController更新裡面的資料,這樣UI就會自動重新整理了。

三、BLoC封裝

不知不覺囉嗦了這麼多,其實這篇文章的目的是對BLoC進行封裝,使Stream類不再暴露在業務程式碼中。

Pub倉庫:

realank_flutter_bloc
國內映象

封裝分成三部分:

RLKBaseBLoC: 資料類,儲存了任意型別的資料data(範型),還有一個改變資料的changeData方法。data供資料使用者來使用,而資料操作者使用changeData改變資料。

你可以繼承它來定義自己的資料內容,同時增加一些方法,用於更新資料。 建立資料例項很簡單,把要儲存的初始資料傳進建構函式就可以了:

class CountBLoC extends RLKBaseBLoC<int> {
//RLKBaseBLoC子類,增加了自加方法
  CountBLoC(int data) : super(data);
  increment() {
    changeData(data + 1);
  }
}

 CountBLoC(0);//RLKBaseBLoC例項
複製程式碼

RLKBloCProvider: 這個是一個Widget,儲存了RLKBaseBLoC例項,example中它包住了整個MaterialApp,它的作用就是為需要用到資料的地方提供資料來源,它只要是所有使用到RLKBaseBLoC資料的widegt的共用根節點就可以。如果你的RLKBaseBLoC資料,只是在頁面A中有很多地方展示,那麼RLKBloCProvider只需要包住頁面A。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RLKBloCProvider(//包住了整個APP,因為資料要被兩個頁面使用
      bloc: CountBLoC(0),//儲存了RLKBaseBLoC例項
      child: MaterialApp(
        theme: ThemeData.light(),
        home: TopPage(),
      ),
    );
  }
}
複製程式碼

RLKBLoCBuilder: 當需要使用RLKBaseBLoC裡面的資料的時候,就在這個widget外圍包住RLKBLoCBuilder,它的作用就是給你提供RLKBaseBLoC資料例項,比如example中,儲存RLKBaseBLoC例項的RLKBloCProvider放在了main.dart中,但是當我想在TopPage中使用RLKBaseBLoC例項的時候,是無法直接拿到這個例項的,通過RLKBLoCBuilder,就可以在buider方法中,以引數的形式拿到RLKBaseBLoC例項了。

Center(child: RLKBLoCBuilder(builder: (BuildContext context, int data, RLKBaseBLoC bloc) {
        return Text(
          'You hit me: $data times',
        );
      }))
複製程式碼

最後,當你想更新資料的時候,同樣是通過RLKBLoCBuilder拿到RLKBaseBLoC例項,然後對資料進行操作

class _STFState extends State<UnderPage> {
  @override
  Widget build(BuildContext context) {
    return RLKBLoCBuilder(builder: (BuildContext context, int data, RLKBaseBLoC bloc) {
      CountBLoC bloc2 = bloc as CountBLoC;
      return Scaffold(
        appBar: AppBar(
          title: Text('Under Page'),
        ),
        body: Center(
            child: Text(
          "You hit me: $data times",
          style: Theme.of(context).textTheme.display1,
        )),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {});
            bloc2.increment();//改變資料
          },
          child: Icon(Icons.add),
        ),
      );
    });
  }
}
複製程式碼

四、總結

當你想要更靈活地管理狀態和UI的時候,直接使用這個package可以讓你遠離複雜的Redux、RxDart、Stream等概念,只需要一個資料類和兩個容器Widegt就可以了,資料和UI很好的分離,庫對程式碼的入侵也比較少。希望可以幫助到你。

相關文章