Flutter完整開發實戰詳解(12、全面深刻理解狀態管理設計)

做爲系列文章的第十二篇,本篇將經過 scope_model 、 BloC 設計模式、flutter_redux 、 fish_redux 來全面深刻分析, Flutter 中你們最爲關心的狀態管理機制,理解各大框架中如何設計實現狀態管理,從而選出你最爲合適的 state 「大管家」。前端

前文:git

在全部 響應式編程 中,狀態管理一直老生常談的話題,而在 Flutter 中,目前主流的有 scope_modelBloC 設計模式flutter_reduxfish_redux 等四種設計,它們的 複雜度上手難度 是逐步遞增的,但同時 可拓展性解耦度複用能力 也逐步提高。github

基於前篇,咱們對 Stream 已經有了全面深刻的理解,後面能夠發現這四大框架或多或少都有 Stream 的應用,不過仍是那句老話,合適纔是最重要,不要爲了設計而設計編程

本文Demo源碼redux

GSYGithubFlutter 完整開源項目設計模式

1、scoped_model

scoped_model 是 Flutter 最爲簡單的狀態管理框架,它充分利用了 Flutter 中的一些特性,只有一個 dart 文件的它,極簡的實現了通常場景下的狀態管理。數組

以下方代碼所示,利用 scoped_model 實現狀態管理只須要三步 :bash

  • 定義 Model 的實現,如 CountModel ,而且在狀態改變時執行 notifyListeners() 方法。
  • 使用 ScopedModel Widget 加載 Model
  • 使用 ScopedModelDescendant 或者 ScopedModel.of<CountModel>(context) 加載 Model 內狀態數據。

是否是很簡單?那僅僅一個 dart 文件,如何實現這樣的效果的呢?後面咱們立刻開始剝析它。app

class ScopedPage extends StatelessWidget {
  final CountModel _model = new CountModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("scoped"),
        ),
        body: Container(
          child: new ScopedModel<CountModel>(
            model: _model,
            child: CountWidget(),
          ),
        ));
  }
}

class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
        return new Column(
          children: <Widget>[
            new Expanded(child: new Center(child: new Text(model.count.toString()))),
            new Center(
              child: new FlatButton(
                  onPressed: () {
                    model.add();
                  },
                  color: Colors.blue,
                  child: new Text("+")),
            ),
          ],
        );
      });
  }
}

class CountModel extends Model {
  static CountModel of(BuildContext context) =>
      ScopedModel.of<CountModel>(context);
  
  int _count = 0;
  
  int get count => _count;
  
  void add() {
    _count++;
    notifyListeners();
  }
}
複製代碼

以下圖所示,在 scoped_model 的整個實現流程中,ScopedModel 這個 Widget 很巧妙的藉助了 AnimatedBuildler框架

由於 AnimatedBuildler 繼承了 AnimatedWidget ,在 AnimatedWidget 的生命週期中會對 Listenable 接口添加監聽,而 Model 剛好就實現了 Listenable 接口,整個流程總結起來就是:

  • Model 實現了 Listenable 接口,內部維護一個 Set<VoidCallback> _listeners
  • Model 設置給 AnimatedBuildler 時, ListenableaddListener 會被調用,而後添加一個 _handleChange 監聽到 _listeners 這個 Set 中。
  • Model 調用 notifyListeners 時,會經過異步方法 scheduleMicrotask 去從頭至尾執行一遍 _listeners 中的 _handleChange
  • _handleChange 監聽被調用,執行了 setState({})

image.png

整個流程是否是很巧妙,機制的利用了 AnimatedWidgetListenable 在 Flutter 中的特性組合,至於 ScopedModelDescendant 就只是爲了跨 Widget 共享 Model 而作的一層封裝,主要仍是經過 ScopedModel.of<CountModel>(context) 獲取到對應 Model 對象,這這個實現上,scoped_model 依舊利用了 Flutter 的特性控件 InheritedWidget 實現。

InheritedWidget

scoped_model 中咱們能夠經過 ScopedModel.of<CountModel>(context) 獲取咱們的 Model ,其中最主要是由於其內部的 build 的時候,包裹了一個 _InheritedModel 控件,而它繼承了 InheritedWidget

爲何咱們能夠經過 context 去獲取到共享的 Model 對象呢?

首先咱們知道 context 只是接口,而在 Flutter 中 context 的實現是 Element ,在 ElementinheritFromWidgetOfExactType 方法實現裏,有一個 Map<Type, InheritedElement> _inheritedWidgets 的對象。

_inheritedWidgets 通常狀況下是空的,只有當父控件是 InheritedWidget 或者自己是 InheritedWidgets 時纔會有被初始化,而當父控件是 InheritedWidget 時,這個 Map 會被一級一級往下傳遞與合併

因此當咱們經過 context 調用 inheritFromWidgetOfExactType 時,就能夠往上查找到父控件的 Widget,從在 scoped_model 獲取到 _InheritedModel 中的Model

2、BloC

BloC 全稱 Business Logic Component ,它屬於一種設計模式,在 Flutter 中它主要是經過 StreamSteamBuilder 來實現設計的,因此 BloC 實現起來也相對簡單,關於 StreamSteamBuilder 的實現原理能夠查看前篇,這裏主要展現如何完成一個簡單的 BloC

以下代碼所示,整個流程總結爲:

  • 定義一個 PageBloc 對象,利用 StreamController 建立 SinkStream
  • PageBloc 對外暴露 Stream 用來與 StreamBuilder 結合;暴露 add 方法提供外部調用,內部經過 Sink 更新 Stream
  • 利用 StreamBuilder 加載監聽 Stream 數據流,經過 snapShot 中的 data 更新控件。

固然,若是和 rxdart 結合能夠簡化 StreamController 的一些操做,同時若是你須要利用 BloC 模式實現狀態共享,那麼本身也能夠封裝多一層 InheritedWidgets 的嵌套,若是對於這一塊有疑惑的話,推薦能夠去看看上一篇的 Stream 解析。

class _BlocPageState extends State<BlocPage> {
  final PageBloc _pageBloc = new PageBloc();
  @override
  void dispose() {
    _pageBloc.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: new StreamBuilder(
            initialData: 0,
            stream: _pageBloc.stream,
            builder: (context, snapShot) {
              return new Column(
                children: <Widget>[
                  new Expanded(
                      child: new Center(
                          child: new Text(snapShot.data.toString()))),
                  new Center(
                    child: new FlatButton(
                        onPressed: () {
                          _pageBloc.add();
                        },
                        color: Colors.blue,
                        child: new Text("+")),
                  ),
                  new SizedBox(
                    height: 100,
                  )
                ],
              );
            }),
      ),
    );
  }
}
class PageBloc {
  int _count = 0;
  ///StreamController
  StreamController<int> _countController = StreamController<int>();
  ///對外提供入口
  StreamSink<int> get _countSink => _countController.sink;
  ///提供 stream StreamBuilder 訂閱
  Stream<int> get stream => _countController.stream;
  void dispose() {
    _countController.close();
  }
  void add() {
    _count++;
    _countSink.add(_count);
  }
}
複製代碼

3、flutter_redux

相信若是是前端開發者,對於 redux 模式並不會陌生,而 flutter_redux 能夠看作是利用了 Stream 特性的 scope_model 升級版,經過 redux 設計模式來完成解耦和拓展。

固然,更多的功能和更好的拓展性,也形成了代碼的複雜度和上手難度 ,由於 flutter_redux 的代碼使用篇幅問題,這裏就不展現全部代碼了,須要看使用代碼的可直接從 demo 獲取,如今咱們直接看 flutter_redux 是如何實現狀態管理的吧。

如上圖,咱們知道 redux 中通常有 StoreActionReducer 三個主要對象,以外還有 Middleware 中間件用於攔截,因此以下代碼所示:

  • 建立 Store 用於管理狀態 。
  • Store 增長 appReducer 合集方法,增長鬚要攔截的 middleware,並初始化狀態。
  • Store 設置給 StoreProvider 這個 InheritedWidget
  • 經過 StoreConnector / StoreBuilder 加載顯示 Store 中的數據。

以後咱們能夠 dispatch 一個 Action ,在通過 middleware 以後,觸發對應的 Reducer 返回數據,而事實上這裏核心的內容實現,仍是 StreamStreamBuilder 的結合使用 ,接下來就讓咱們看看這個流程是如何聯動起來的吧。

class _ReduxPageState extends State<ReduxPage> {

  ///初始化store
  final store = new Store<CountState>(
    /// reducer 合集方法
    appReducer,
    ///中間鍵
    middleware: middleware,
    ///初始化狀態
    initialState: new CountState(count: 0),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("redux"),
        ),
        body: Container(
          /// StoreProvider InheritedWidget
          /// 加載 store 共享
          child: new StoreProvider(
            store: store,
            child: CountWidget(),
          ),
        ));
  }
}
複製代碼

以下圖所示,是 flutter_redux 從入口到更新的完整流程圖,整理這個流程其中最關鍵有幾個點是:

  • StoreProviderInheritedWidgets ,因此它能夠經過 context 實現狀態共享。
  • StreamBuilder / StoreConnector 的內部實現主要是 StreamBuilder
  • Store 內部是經過 StreamController.broadcast 建立的 Stream ,而後在 StoreConnector 中經過 Streammaptransform 實現小狀態的變換,最後更新到 StreamBuilder

那麼如今看下圖流程有點暈?下面咱們直接分析圖中流程。

能夠看出整個流程的核心仍是 Stream ,基於這幾個關鍵點,咱們把上圖的流程整理爲:

  • 一、 Store 建立時傳入 reducer 對象和 middleware 數組,同時經過 StreamController.broadcast 建立了 _changeController 對象。
  • 二、 Store 利用 middleware_changeController 組成了一個 NextDispatcher 方法數組 ,並把 _changeController 所在的 NextDispatcher 方法放置在數組最後位置。
  • 三、 StoreConnector 內經過 Store_changeController 獲取 Stream ,並進行了一系列變換後,最終 Stream 設置給了 StreamBuilder
  • 四、當咱們調用 Stroedispatch 方法時,咱們會先進過 NextDispatcher 數組中的一系列 middleware 攔截器,最終調用到隊末的 _changeController 所在的 NextDispatcher
  • 五、最後一個 NextDispatcher 執行時會先執行 reducer 方法獲取新的 state ,而後經過 _changeController.add 將狀態加載到 Stream 流程中,觸發 StoreConnectorStreamBuilder 更新數據。

若是對於 Stream 流程不熟悉的還請看上篇。

如今再對照流程圖會不會清晰不少了?

flutter_redux 中,開發者的每一個操做都只是一個 Action ,而這個行爲所觸發的邏輯徹底由 middlewarereducer 決定,這樣的設計在必定程度上將業務與UI隔離,同時也統一了狀態的管理。

好比你一個點擊行爲只是發出一個 RefrshAction ,可是經過 middleware 攔截以後,在其中異步處理完幾個數據接口,而後從新 dispatchAction1Action2Action3 去更新其餘頁面, 相似的 redux_epics 庫就是這樣實現異步的 middleware 邏輯。

4、fish_redux

若是說 flutter_redux 屬於相對複雜的狀態管理設置的話,那麼閒魚開源的 fish_redux 可謂 「不走尋常路」 了,雖然是基於 redux 原有的設計理念,同時也有使用到 Stream ,可是相比較起來整個設計徹底是 超脫三界,若是是前面的都是簡單的拼積木,那是 fish_redux 就是積木界的樂高。

由於篇幅緣由,這裏也只展現部分代碼,其中 reducer 仍是咱們熟悉的存在,而閒魚在這 redux 的基礎上提出了 Comoponent 的概念,這個概念下 fish_redux 是從 ContextWidget 等地方就開始全面「入侵」你的代碼,從而帶來「超級賽亞人」版的 redux

以下代碼所示,默認狀況咱們須要:

  • 繼承 Page 實現咱們的頁面。
  • 定義好咱們的 State 狀態。
  • 定義 effectmiddlewarereducer 用於實現反作用、中間件、結果返回處理。
  • 定義 view 用於繪製頁面。
  • 定義 dependencies 用戶裝配控件,這裏最騷氣的莫過於重載了 + 操做符,而後利用 ConnectorState 挑選出數據,而後經過 Component 繪製。

如今看起來使用流程是否是變得複雜了?

可是這帶來的好處就是 複用的顆粒度更細了,裝配和功能更加的清晰。 那這個過程是如何實現的呢?後面咱們將分析這個複雜的流程。

class FishPage extends Page<CountState, Map<String, dynamic>> {
  FishPage()
      : super(
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          ///配置 View 顯示
          view: buildView,
          ///配置 Dependencies 顯示
          dependencies: Dependencies<CountState>(
              slots: <String, Dependent<CountState>>{
                ///經過 Connector() 從 大 state 轉化處小 state
                ///而後將數據渲染到 Component
                'count-double': DoubleCountConnector() + DoubleCountComponent()
              }
          ),
          middleware: <Middleware<CountState>>[
            ///中間鍵打印log
            logMiddleware(tag: 'FishPage'),
          ]
  );
}

///渲染主頁
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
      appBar: AppBar(
        title: new Text("fish"),
      ),
      body: new Column(
        children: <Widget>[
          ///viewService 渲染 dependencies
          viewService.buildComponent('count-double'),
          new Expanded(child: new Center(child: new Text(state.count.toString()))),
          new Center(
            child: new FlatButton(
                onPressed: () {
                  ///+
                  dispatch(CountActionCreator.onAddAction());
                },
                color: Colors.blue,
                child: new Text("+")),
          ),
          new SizedBox(
            height: 100,
          )
        ],
      ));
}
複製代碼

以下大圖所示,整個聯動的流程比 flutter_redux 複雜了更多( 若是看不清能夠點擊大圖 ),而這個過程咱們總結起來就是:

  • 一、Page 的構建須要 StateEffectReducerviewdependenciesmiddleware 等參數。

  • 二、Page 的內部 PageProvider 是一個 InheritedWidget 用戶狀態共享。

  • 三、Page 內部會經過 createMixedStore 建立 Store 對象。

  • 四、Store 對象對外提供的 subscribe 方法,在訂閱時會將訂閱的方法添加到內部 List<_VoidCallback> _listeners

  • 五、Store 對象內部的 StreamController.broadcast 建立出了 _notifyController 對象用於廣播更新。

  • 六、Store 對象內部的 subscribe 方法,會在 ComponentState 中添加訂閱方法 onNotify若是調用在 onNotify 中最終會執行 setState更新UI。

  • 七、Store 對象對外提供的 dispatch 方法,執行時內部會執行 4 中的 List<_VoidCallback> _listeners,觸發 onNotify

  • 八、Page 內部會經過 Logic 建立 Dispatch ,執行時經歷 Effect -> Middleware -> Stroe.dispatch -> Reducer -> State -> _notifyController -> _notifyController.add(state) 等流程。

  • 九、以上流程最終就是 Dispatch 觸發 Store 內部 _notifyController , 最終會觸發 ComponentState 中的 onNotify 中的setState更新UI

是否是有不少對象很陌生?

確實 fish_redux 的總體流程更加複雜,內部的 ContxtSysComponetViewSerivceLogic 等等概念設計,這裏由於篇幅有限就不詳細拆分展現了,但從整個流程能夠看出 fish_redux控件到頁面更新,全都進行了新的獨立設計,而這裏面最有意思的,莫不過 dependencies

以下圖所示,得益於fish_redux 內部 ConnOpMixin 中對操做符的重載,咱們能夠經過 DoubleCountConnector() + DoubleCountComponent() 來實現Dependent 的組裝。

Dependent 的組裝中 Connector 會從總 State 中讀取須要的小 State 用於 Component 的繪製,這樣很好的達到了 模塊解耦與複用 的效果。

而使用中咱們組裝的 dependencies 最後都會經過 ViewService 提供調用調用能力,好比調用 buildAdapter 用於列表能力,調用 buildComponent 提供獨立控件能力等。

能夠看出 flutter_redux 的內部實現複雜度是比較高的,在提供組裝、複用、解耦的同時,也對項目進行了必定程度的入侵,這裏的篇幅可能不能很全面的分析 flutter_redux 中的整個流程,可是也能讓你理解整個流程的關鍵點,細細品味設計之美。

自此,第十二篇終於結束了!(///▽///)

資源推薦

完整開源項目推薦:
文章

《Flutter完整開發實戰詳解(1、Dart語言和Flutter基礎)》

《Flutter完整開發實戰詳解(2、 快速開發實戰篇)》

《Flutter完整開發實戰詳解(3、 打包與填坑篇)》

《Flutter完整開發實戰詳解(4、Redux、主題、國際化)》

《Flutter完整開發實戰詳解(5、 深刻探索)》

《Flutter完整開發實戰詳解(6、 深刻Widget原理)》

《Flutter完整開發實戰詳解(7、 深刻佈局原理)》

《Flutter完整開發實戰詳解(8、 實用技巧與填坑)》

《Flutter完整開發實戰詳解(9、 深刻繪製原理)》

《Flutter完整開發實戰詳解(10、 深刻圖片加載流程)》

《Flutter完整開發實戰詳解(11、全面深刻理解Stream)》

《Flutter完整開發實戰詳解(12、全面深刻理解狀態管理設計)》

《跨平臺項目開源項目推薦》

《移動端跨平臺開發的深度解析》

咱們還會再見嗎?
相關文章
相關標籤/搜索