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

戀貓de小郭發表於2019-04-30

作為系列文章的第十二篇,本篇將通過 scope_model 、 BloC 設計模式、flutter_redux 、 fish_redux 來全面深入分析, Flutter 中大家最為關心的狀態管理機制,理解各大框架中如何設計實現狀態管理,從而選出你最為合適的 state “大管家”。

文章彙總地址:

Flutter 完整實戰實戰系列文章專欄

Flutter 番外的世界系列文章專欄

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

基於前篇,我們對 Stream 已經有了全面深入的理解,後面可以發現這四大框架或多或少都有 Stream 的應用,不過還是那句老話,合適才是最重要,不要為了設計而設計

本文Demo原始碼

GSYGithubFlutter 完整開源專案

一、scoped_model

scoped_model 是 Flutter 最為簡單的狀態管理框架,它充分利用了 Flutter 中的一些特性,只有一個 dart 檔案的它,極簡的實現了一般場景下的狀態管理。

如下方程式碼所示,利用 scoped_model 實現狀態管理只需要三步 :

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

是不是很簡單?那僅僅一個 dart 檔案,如何實現這樣的效果的呢?後面我們馬上開始剝析它。

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

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

三、flutter_redux

相信如果是前端開發者,對於 redux 模式並不會陌生,而 flutter_redux 可以看做是利用了 Stream 特性的 scope_model 升級版,通過 redux 設計模式來完成解耦和擴充。

當然,更多的功能和更好的擴充性,也造成了程式碼的複雜度和上手難度 ,因為 flutter_redux 的程式碼使用篇幅問題,這裡就不展示所有程式碼了,需要看使用程式碼的可直接從 demo 獲取,現在我們直接看 flutter_redux 是如何實現狀態管理的吧。

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

如上圖,我們知道 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

那麼現在看下圖流程有點暈?下面我們直接分析圖中流程。

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

可以看出整個流程的核心還是 Stream ,基於這幾個關鍵點,我們把上圖的流程整理為:

  • 1、 Store 建立時傳入 reducer 物件和 middleware 陣列,同時通過 StreamController.broadcast 建立了 _changeController 物件。
  • 2、 Store 利用 middleware_changeController 組成了一個 NextDispatcher 方法陣列 ,並把 _changeController 所在的 NextDispatcher 方法放置在陣列最後位置。
  • 3、 StoreConnector 內通過 Store_changeController 獲取 Stream ,並進行了一系列變換後,最終 Stream 設定給了 StreamBuilder
  • 4、當我們呼叫 Stroedispatch 方法時,我們會先進過 NextDispatcher 陣列中的一系列 middleware 攔截器,最終呼叫到隊末的 _changeController 所在的 NextDispatcher
  • 5、最後一個 NextDispatcher 執行時會先執行 reducer 方法獲取新的 state ,然後通過 _changeController.add 將狀態載入到 Stream 流程中,觸發 StoreConnectorStreamBuilder 更新資料。

如果對於 Stream 流程不熟悉的還請看上篇。

現在再對照流程圖會不會清晰很多了?

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

比如你一個點選行為只是發出一個 RefrshAction ,但是通過 middleware 攔截之後,在其中非同步處理完幾個資料介面,然後重新 dispatchAction1Action2Action3 去更新其他頁面, 類似的 redux_epics 庫就是這樣實現非同步的 middleware 邏輯。

四、fish_redux

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

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

因為篇幅原因,這裡也只展示部分程式碼,其中 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 複雜了更多( 如果看不清可以點選大圖 ),而這個過程我們總結起來就是:

  • 1、Page 的構建需要 StateEffectReducerviewdependenciesmiddleware 等引數。

  • 2、Page 的內部 PageProvider 是一個 InheritedWidget 使用者狀態共享。

  • 3、Page 內部會通過 createMixedStore 建立 Store 物件。

  • 4、Store 物件對外提供的 subscribe 方法,在訂閱時會將訂閱的方法新增到內部 List<_VoidCallback> _listeners

  • 5、Store 物件內部的 StreamController.broadcast 建立出了 _notifyController 物件用於廣播更新。

  • 6、Store 物件內部的 subscribe 方法,會在 ComponentState 中新增訂閱方法 onNotify如果呼叫在 onNotify 中最終會執行 setState更新UI。

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

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

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

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

是不是有很多物件很陌生?

確實 fish_redux 的整體流程更加複雜,內部的 ContxtSysComponetViewSerivceLogic 等等概念設計,這裡因為篇幅有限就不詳細拆分展示了,但從整個流程可以看出 fish_redux控制元件到頁面更新,全都進行了新的獨立設計,而這裡面最有意思的,莫不過 dependencies

如下圖所示,得益於fish_redux 內部 ConnOpMixin 中對操作符的過載,我們可以通過 DoubleCountConnector() + DoubleCountComponent() 來實現Dependent 的組裝。

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

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

而使用中我們組裝的 dependencies 最後都會通過 ViewService 提供呼叫呼叫能力,比如呼叫 buildAdapter 用於列表能力,呼叫 buildComponent 提供獨立控制元件能力等。

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

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

資源推薦

完整開源專案推薦:

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

相關文章