Flutter 記錄 - Flutter State Management (Redux)使用介紹

路馬發表於2020-02-18

Flutter 記錄 - Flutter State Management (Redux)使用介紹

Flutter 及狀態管理庫簡述


flutter 是由谷歌公司於 2015 年推出的移動 UI 框架。其選用 Dart 作為開發語言,內建 Material Design 和 Cupertino 兩種設計風格 Widget 部件,使得它可以快速在 ios、android 裝置上構建高效、高品質、高流暢度的使用者 UI 介面。

Flutter 在很多設計理念上參考了 React 的設計理念,因此在狀態管理庫的選擇和業務流處理上,也可以選擇和 React 類似的解決方案,其中最具代表的便是 Redux。

為什麼要使用狀態管理庫


在宣告式 UI 元件開發的過程中,一個 Flutter 元件呈現出樹狀結構,抽象出來大概長這樣:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

可以發現,這種結構下,我們通過引數去傳遞一些資料,由父元件向子元件傳遞資料,子元件負責根據資料進行 UI 元件的渲染,這種資料流是自上而下流動的。

但是有時,我們會需要在 app 的不同介面中共享應用程式的某些狀態或者是資料。如果他們沒有任何關係,我們可能需要很多額外操作來做元件之間的通訊,傳遞這些資料。這時候,我們便需要一種獨立在元件之後的資料來源和狀態庫,來管理這些公共狀態和資料。

應用狀態的分類


在 Flutter 專案中,應用的狀態分為短時狀態應用狀態:

  • 短時狀態

短時狀態也稱區域性狀態。比如一個 tab 選項卡中,當前被選中的 tab 對應的序列號;一個輸入框當前輸入的值都可以稱為短時狀態(區域性狀態)。在一般場景下,對於這類狀態,往往不是其他元件所關心的,也不需要我們幫助使用者記住這種狀態。即使應用重啟,這些狀態恢復到預設狀態,也不會對應用造成太大影響,這種狀態的丟失是可以接受的。

  • 應用狀態

應用狀態,也稱共享狀態,全域性狀態等。最具代表性的,便是 loginFlag(一個用於標識使用者當前是否登入的欄位)了。這種狀態,往往影響了整個應用的邏輯和UI的渲染,因為使用者是否登入決定我們是否返回當前使用者的個人資訊等。而且很多時候,登入狀態一旦設定,我們可能在一段時間內要記住這種狀態,即使應用重啟,這種狀態也不可丟失。類似這種的狀態和資料,便稱為應用狀態。

狀態管理庫


在 Flutter 中,可提供應用狀態管理的工具和第三方元件庫有很多,如:Redux, Provider, BloC, RxDart 等。這次記錄主要提供如下三種狀態庫的介紹及使用:

  • Redux 詳細的使用介紹及程式設計規範
  • BloC 模式詳細的使用
  • Provider 介紹及使用

我們通過使用 Redux,BloC 及 Provider 從分別完成一個資料流的傳遞,來對比這三者的使用。

需求概述


完成一個應用,通過提交使用者資訊來登入應用,記錄下使用者提交的資訊,並展示。

實現的效果如下:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

需求實現(Redux 版本)


  • 匯入依賴

    使用 Redux ,我們需要先匯入使用 Dart 編寫的 Redux 狀態庫,還需要匯入用於連線 Flutter 應用和Redux狀態的連結庫flutter-redux:

    在 pubspec.yml 檔案中匯入依賴, 並在命令列執行 flutter pub get 從遠端伺服器獲取依賴:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

  • 設計狀態模型 Model

    根據前面的需求概述,我們的應用狀態根 AppState 中應至少包含下面兩個模組的狀態:

      * 全域性狀態 => globleState 用於儲存全域性的 **應用狀態**
      * 使用者狀態 => userState 用於儲存使用者相關的**應用狀態**
    複製程式碼

  • 生成狀態 UserState model 類

    依據前面對應用狀態樹的設計,我們首先完成 UserState Model 類的建立:

    新建 UserState model 類:

    /// model/user_model.dart
    
    /// store user state
    class UserState {
      String name;
      String email;
      double age;
    
      UserState({
        @required this.name, 
        @required this.email, 
        @required this.age
      });
    }
複製程式碼

在使用 Redux 進行狀態管理時,通常會需要給應用的狀態一些預設值,因此可以通過命名建構函式為 UserState 提供一個用於初始化的建構函式 initState:

    /// model/user_model.dart
    class UserState {
      String name;
      String email;
      double age;
    
      UserState({
        @required this.name, 
        @required this.email, 
        @required this.age
      });
      
      UserState.initState(): name = 'redux', email = 'redux@gmail.com', age = 10;
    }
複製程式碼

通過構造方法,我們便可以在合適的地方呼叫 initState 建構函式為 UserState 類提供預設值。

使用過 Redux 的都知道,在 Redux 中,所有的狀態都由 Reducer 純函式生成,Reducer 函式通過接受新、舊狀態進行合併,生成新的狀態返回到狀態樹中。 為了防止我們上一次的狀態丟失,我們應該將上一次的狀態記錄下來並與新狀態進行合併處理,因此我們還需要在 UserState 類中新增一個 copy 方法用於狀態的合併:

關於純函式可以參考函數語言程式設計

    /// model/user_model.dart
    class UserState {
      String name;
      String email;
      double age;
    
      UserState({
        @required this.name, 
        @required this.email, 
        @required this.age
      });
      
      UserState.initState(): name = 'redux', email = 'redux@gmail.com', age = 10;
      
      UserState copyWith(UserModel userModel) {
        return UserState(
          name: userModel.name ?? this.name,
          email: userModel.email ?? this.email,
          age: userModel.age ?? this.age
        );
      }
    }
複製程式碼

我們在類中編寫了一個 copyWith 方法,這個方法針對當前例項,接受使用者資訊 user model, 通過判斷是否有新值傳入來決定是否返回老狀態。

這樣一個 UserState 的類便建立好了。


  • 編寫 GlobalState, AppState model 類

    與 UserState 類似,我們快速完成 GlobalState, AppState 類

    GlobalState model 類:

    /// model/global_model.dart
    import 'package:flutter/material.dart';
    
    /// store global state
    class GlobalState {
      bool loginFlag;
    
      GlobalState({
        @required this.loginFlag
      });
      
      GlobalState.initState(): loginFlag = false;
    
      GlobalState copyWith(loginFlag) {
        return GlobalState(
          loginFlag: loginFlag ?? this.loginFlag
        );
      }
    }
複製程式碼

App State model 類:

    /// model/app_model.dart
    import 'package:flutter_state/Redux/model/global_model.dart';
    import 'package:flutter_state/Redux/model/user_model.dart';
    
    /// APP global
    class AppState {
      UserState userState;
      GlobalState globalState;
    
      AppState({ this.userState, this.globalState });
      
      AppState copyWith({
        UserState userState,
        GlobalState globalState,
      }) {
        return AppState(
          userState: userState ?? this.userState,
          globalState: globalState ?? this.globalState
        );
      }
    }
複製程式碼

  • 建立 store 倉庫

    接下里,我們需要在專案根目錄中建立一個 store 資料夾,用於存放專案中所需要的 action 和 reducer 檔案:

    * - store
    *   - action.dart
    *   - reducer.dart
複製程式碼
  • 編寫 action

    依據前面的需求,我們在 action 中編寫專案中需要用到的 action 動作類。

    // action.dart
    import 'package:flutter_state/Redux/model/user_model.dart';
    
    // User Action
    enum UserAction {
      SetUserInfo,
      ClearUserInfo,
    }
    
    class SetUserInfo {
      final UserModel userModel;
      
      SetUserInfo(this.userModel);
    }
    
    class ClearUserInfo {}
    
    // Global Action
    enum GlobalAction {
      SetLoginFlag,
      LogoutSystem
    }
    
    class SetLoginFlag {
      final bool loginFlag;
    
      SetLoginFlag({ this.loginFlag });
    }
    
    class LogoutSystem {}
複製程式碼

通常情況下,一個 Action 動作由 Type 和 Payload 組成,Type 標識動作型別,Payload 作為函式載體。 由於 dart 靜態語言的一些特性,使用類來作為資料載體,方便擴充和進行資料邏輯處理。 用類名、字串還是列舉來定義你的 Action 動作型別,決定了你在 Reducer 中如何去判斷 Action 的動作型別進而進行相關的邏輯處理。實際業務中可根據業務場景靈活處理。

  • 編寫 reducer 純函式

定義好相關的 action 動作後,我們編寫對應的 reducer 函式。前面提到過,Reducer 函式通過接受新、舊狀態進行合併,生成新的狀態返回到狀態樹中:

    // reducer.dart
    ...
    import 'package:redux/redux.dart';
    
    UserState userSetUpReducer(UserState userState, action) {
      if (action is SetUserInfo) {
        return userState.copyWith(action.userModel);
      } else if (action is ClearUserInfo) {
        return UserState.initState();
      } else {
        return userState;
      }
    }
      
    GlobalState globalStatusReducer(GlobalState globalState, action) {
      if (action is SetLoginFlag) {
        return globalState.copyWith(action.loginFlag);
      } else if (action is LogoutSystem) {
        return GlobalState.initState();
      } else {
        return globalState;
      }
    }
複製程式碼

上面的程式碼中,分別定義了兩個純函式 userSetUpReducer, globalStatusReducer。他們的邏輯非常簡單,通過判斷 action 動作型別,對相應的 State 進行合併操作,生成新的狀態並返回。

由於我們使用`類`去作為 Action 進行派發,因此在 reducer 中處理對應 action 時,可通過 is 來判斷類的型別

  • 編寫頂層 appReducer 函式

    完成子模組 reducer 函式的編寫後,我們需要完成元件狀態樹的頂層函式的 appReducer。appReducer 維護了我們應用最頂層狀態,我們在此處將對應的模組狀態交給他們的 reducer 函式進行處理:

    import 'package:flutter_state/Redux/model/app_model.dart';
    import 'package:flutter_state/Redux/store/reducer.dart';
    
    AppState appReducer(AppState appState, action) {
      return appState.copyWith(
        userState: userReducers(appState.userState, action),
        globalState: globalReducers(appState.globalState, action),
      );
    }
複製程式碼
appReducer 函式,接受 AppState,並通過 copyWith 方法,將 userState 和 globalState 狀態分別交由他們對應的 reducer 函式進行處理。
複製程式碼
  • 在應用中關聯 store

    一般場景下,我們只在業務最頂層維護一個全域性的 store , 頂層的 store 通過 接受 reducer 函式來進行狀態的合併與分發處理

    接下來,我們在 應用入口處初始化 store 倉庫,並繫結到應用中:

    // main.dart
    ...
    import 'package:flutter_redux/flutter_redux.dart';
    import 'package:redux/redux.dart';
    
    // before
    void main() {  
        runApp(MyApp())
    };
    
    // after
    void main() {
      final store = Store<AppState>(
        appReducer,
        initialState: AppState(
          globalState: GlobalState.initState(),
          userState: UserState.initState(),
        )
      );
    
      runApp(
        StoreProvider<AppState>(
          store: store,
          child: MyApp(),
        )
      );
    }
    
    ...
複製程式碼

上面的程式碼,通過 Redux 中 store, 我們初始化了一個 store 倉庫,在 initialState 裡我們設定了應用的初始狀態。

之後我們通過 flutter_redux 中 StoreProvider 方法,將 store 和 應用(MyApp)進行了關聯。

這樣我們的 store 倉庫便匯入完成了。

  • 建立 UI 測試元件

    新建 redux_perview 元件, 在其中完成檢視的編輯:

    // redux_perview.dart
    class ReduxPerviewPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        ....
        child: Column(
          children: <Widget>[
            Text('Redux Perview: ', style: TextStyle(fontSize: 40),),
            SizedBox(height: 40,),
            Text('Name: ', style: _textStyle),
            Text('Email: ', style: _textStyle),
            Text('Age: ', style: _textStyle),
          ]
        ),
      }
    }
    複製程式碼

    Perview 負責對使用者的資訊進行展示,當使用者沒有登入時,該元件隱藏並使用 store 預設值。

    新建 redux_trigger 元件,再其完成用於使用者輸入 UI 的繫結:

    // redux_trigger
    class ReduxTriggerPage extends StatelessWidget {
        static final formKey = GlobalKey<FormState>();
    
        final UserModel userModel = new UserModel();
        
        Widget _loginForm (BuildContext context, Store) {
            ...
            Column(
                children: [
                  ...
                  TextFormField(
                    decoration: InputDecoration(labelText: 'Name'),
                    onSaved: (input) => userModel.name = input,
                  ),
                  ...
                ]
            )
        }
        
        @override
        Widget build(BuildContext context) {
            return _loginForm(context)
        } 
    }
    複製程式碼

    Trigger 元件接受使用者輸入的資訊,提交到 store 倉庫。該元件在輸入完畢登入成功之後,處於影藏狀態

    此時執行效果如下:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

  • 在 ReduxPerviewPage 元件中使用狀態

    接下來,我們要在 UI 元件中繫結倉庫中的狀態。

    Flutter_redux 為我們提供了兩個函式元件 StoreConnector 和 StoreBuilder,在文章的最後會針對這兩個方法的使用場景做進一步的介紹。

    在此處,我們使用 StoreBuilder 完成對 perview 展示頁面的繫結:

    為了防止巢狀過深,將 UI 部分抽離為 _perviewWidget 方法

    class ReduxPerviewPage extends StatelessWidget {
    
      Widget _perviewWidget(BuildContext context, Store<AppState> store) {
        ...
        UserState userState = store.state.userState;
        ...
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center, 
          children: <Widget>[
            ...
            Text('Name: ${userState.name}', style: _textStyle),
          ]
        )
      }
    
      @override
      Widget build(BuildContext context) {
        return StoreBuilder<AppState>(
          builder: (BuildContext context, Store<AppState> store) => 
            store.state.globalState.loginFlag ? _perviewWidget(context, store) : Center(child: Text('請登入'),)
        );
      }
    }
    複製程式碼

    上面的程式碼中,我們使用 StoreBuilder 的 builder 方法,拿到了上下文 context 和 store 的倉庫的狀態。通過邏輯判斷,將狀態傳入 _perviewWidget 完成頁面 Store 狀態至 UI 資料的繫結。

  • 在 ReduxTrggier 改變頁面狀態

    接下來我們在 trigger 元件中提交 action 資訊,來改變 state 中的狀態:

    trigger 元件

    Flutter 記錄 - Flutter State Management (Redux)使用介紹

    class ReduxTriggerPage extends StatelessWidget {
    
      static final formKey = GlobalKey<FormState>();
    
      final UserModel userModel = new UserModel();
    
      Widget _loginForm (BuildContext context, Store<AppState> store) {
        return Center(
          child: Container(
            height: (MediaQuery.of(context).size.height - 120) / 2,
            padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30),
            child: Form(
              key: formKey,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  TextFormField(
                    decoration: InputDecoration(labelText: 'Name'),
                    onSaved: (input) => userModel.name = input,
                  ),
                  TextFormField(
                    decoration: InputDecoration(labelText: 'Email'),
                    onSaved: (input) => userModel.email = input,
                  ),
                  TextFormField(
                    decoration: InputDecoration(labelText: 'Age'),
                    onSaved: (input) => userModel.age = double.parse(input),
                  ),
                  FlatButton(
                    onPressed: () {
                      formKey.currentState.save();
                      // 提交 action 動作
                      StoreProvider.of<AppState>(context).dispatch(new SetLoginFlag(loginFlag: true));
                      StoreProvider.of<AppState>(context).dispatch(new SetUserInfo(userModel));
                      
                      formKey.currentState.reset();
                    },
                    child: Text('遞交資訊'),
                    color: Colors.blue,
                    textColor: Colors.white,
                  )
                ]
              ),
            ),
          )
        );
      }
      @override
      Widget build(BuildContext context) {
        return StoreBuilder<AppState>(
          builder: (BuildContext context, Store<AppState> store) => 
            store.state.globalState.loginFlag ? Text('') : _loginForm(context, store)
        );
      }
    }
複製程式碼

上面的程式碼中,我們在 StatelessWidget widget 無狀態元件中使用form 表單,對例項化的 userModel 物件進行賦值。 使用 Flutter_redux 提供的 StoreProvider 類,通過呼叫 of 靜態方法,便可以拿到 store 例項

Flutter 記錄 - Flutter State Management (Redux)使用介紹

拿到例項以後,便可通過 dispatch 方法傳送對應的 action,redux 接受到 action 之後,便會交由 reducer 函式進行處理了。

到這裡,redux 業務流的引入便完成了。

StoreProvider 通過實現 InheritedWidget 機制實現,原理類似 redux 中的 context,當 store 發生改變的時候,StoreConnector 或者 StoreBuilder 狀態的得到最新狀態,此時通過 StoreConnector 或 StoreBuilder 包裹的元件便都會得到更新。


使用 StoreConnector 的優化程式碼


flutter_redux 提供了兩個 builder 元件:StoreConnector 和 StoreBuilder。這兩個元件在實現原理上基本一致,在業務中使用時,我們應該針對不同的業務場景來選擇不同的連結元件來最大程度解耦我們的應用的。

上面 redux_perview 例子,使用 StoreConnector 重構:

// redux_perview.dart
class ReduxPerviewPage extends StatelessWidget {

  Widget _perviewWidget(BuildContext context, AppState appState) {
    UserState userState = appState.userState;

    return 
        ...
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center, 
          children: <Widget>[
            FlutterLogo(),
            Text('Redux Perview:', style: TextStyle(fontSize: 40),),
            SizedBox(height: 40,),
            Text('Name: ${userState.name}', style: _textStyle),
            Text('Email: ${userState.email}', style: _textStyle),
            Text('Age: ${userState.age}', style: _textStyle),
            ...
          ]
        ),
        ...
  }

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, AppState>(
      converter: (Store<AppState> store) => store.state,
      builder: (BuildContext context, AppState appState) => 
        appState.globalState.loginFlag ? _perviewWidget(context, appState) : Center(child: Text('請登入'),)
    );
  }
}

複製程式碼

StoreConnector 呼叫 converter 函式,將 Store 的對映為 appState。 通過 StoreConnector,我們可以對 store 的引數進行處理,對映為元件需要的狀態供元件進行使用。

StoreConnector 接受兩個泛型引數:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

通過原始碼,可以看到第一個泛型,需要傳入 Store 宣告,第二引數可以傳入你需要對映的型別。在 Flutter_redux 內部,converter 方法會在 initState 應用鉤子初始化的時候呼叫:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

有了這層轉換,我們便可以去做邏輯層的抽象,將我們的例項對映成 viewModel,這樣便可進一步對邏輯層進行抽離,前面 redux_perview 例子,我們做如下改造:

新建一個 PerviewViewModel 類:

class PerviewViewModel {
  final UserState userModel;
  final bool loginFlag;
  final Function() clearUserInfo;
  final Function() logout;

  PerviewViewModel({
    this.userModel,
    this.loginFlag,
    this.clearUserInfo,
    this.logout
  });

  factory PerviewViewModel.create(Store<AppState> store) {
    _clearUserInfo() {
      store.dispatch(new ClearUserInfo());
    }

    _logout() {
      store.dispatch(new LogoutSystem());
    }

    return PerviewViewModel(
      userModel: store.state.userState,
      loginFlag: store.state.globalState.loginFlag,
      clearUserInfo: _clearUserInfo,
      logout: _logout
    );
  }
}
複製程式碼

在 previewModelView 中,我們通過建構函式 create 傳入 store 例項,將 store 和 UI 相關的業務,全部抽離到 viewModel 當中。修改 converter 方法,將對映型別修改為 previewModelView:

...
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, PerviewViewModel>(
      converter: (Store<AppState> store) => PerviewViewModel.create(store),
      builder: (BuildContext context, PerviewViewModel model) => 
        model.loginFlag ? _perviewWidget(context, model) : Center(child: Text('請登入'),)
    );
  }
...
複製程式碼

此時,我們傳入的 UI 元件的資料變更為 PerviewViewModel 例項,修改 _perviewWidget Ui 元件程式碼:

...

  Widget _perviewWidget(BuildContext context, PerviewViewModel model) {
    ...
        FlutterLogo(),
        Text('Redux Perview:', style: TextStyle(fontSize: 40),),
        SizedBox(height: 40,),
        Text('Name: ${model.userModel.name}', style: _textStyle),
        Text('Email: ${model.userModel.email}', style: _textStyle),
        Text('Age: ${model.userModel.age}', style: _textStyle),

        FlatButton(
          onPressed: () {
            model.clearUserInfo();
            model.logout();
          },
          child: Text('logout'),
          color: Colors.grey,
          textColor: Colors.black,
        )
...
複製程式碼

可以發現,我們通過 viewModel 的形式,將原本耦合在業務中邏輯程式碼,抽離到 viewModel 中,針對於對 store 狀態的組合和邏輯程式碼,就可以與UI 元件進行解耦了。

這種模式在應對一些複雜的業務過程中,可以有效的幫助我們去解綁 UI 和 store 層。將邏輯和 UI 分離,不僅僅有利於我們保持 UI 元件的簡潔,針對 viewModel 的邏輯,更方便了我們去做業務的單元測試。在未來業務修改和移植的時候,也會更加清晰。

使用 StoreConnector 還是 StoreBuilder


通過上面的案例可以發現,使用 StoreConnector 可以有效解耦業務,在一些簡單的場景下,使用 StoreConnector 可能讓程式碼量增多。因此在使用 StoreConnector 還是 StoreBuilder 上,我覺得在一些簡單場景下,我們應該儘可能抽取 UI 元件,對狀態的設計和數量要有一定控制,這時便可以使用 StoreBuilder 直接處理 store 相關邏輯。

但是對於一些複雜的業務場景,需要頻次對 store 進行操作的時候,為了日後元件的複用及程式碼清晰度,可以使用 StoreConnector 對業務層進行抽象,這樣對日後的維護有很大好處。

redux 中介軟體的使用


在業務開發的過程中,我們可以在 viemModel 中處理我們的業務邏輯。但是對於一些非同步問題,如 service api的呼叫,應該在何處進行呢?redux 基於一種通用的設計,解決了非同步 Action 的呼叫問題,那便是加入 middleware(中介軟體)。

  • 到底什麼是中介軟體呢?

    中介軟體其實是負責業務副作用,並處理業務邏輯相關工作(通常是非同步的)的實體。 所有的 action 在到達 reducer 函式前都會經過中介軟體。每個中介軟體通過 next 函式,呼叫下一個中介軟體,並在 next 中傳遞 action 動作,進而完成由 中介軟體 => reducer => 中介軟體 的排程過程。

  • 中介軟體使用示例

    我們結合兩個場景來演示中介軟體的使用。

    前面在 redux_trggier 元件中,我們通過直接觸發 setLoginFlag action 來完成了登入狀態的設定。事實上在真實業務場景中,我們應先對 setUserInfo action 中的入參進行一定的校驗後,在傳送給伺服器進行身份驗證。通過 http 請求拿到後臺返回的約定狀態碼後,再根據狀態碼判斷使用者是否登入成功並觸發對應的 action

    針對這個業務場景,我們使用中介軟體來解決。

    首先 action 中新增一些 新的 action 動作用與 action 的派發和相關業務:

    // store/action.dart
    
    // 使用者承載頁面入參的action
    class ValidateUserLoginFields {
      final UserModel userModel;
    
      ValidateUserLoginFields(this.userModel);
    }
    
    // 使用者承載入參錯誤資訊的action
    class LoginFormFieldError {
      final String nameErrorMessage;
      final String emailErrorMessage;
      final String ageErrorMessage;
    
      LoginFormFieldError(
        this.nameErrorMessage, 
        this.emailErrorMessage, 
        this.ageErrorMessage
      );
    }
    
    // 用於傳送使用者資訊的 action
    class FetchUserLogin {
      final UserModel userModel;
    
      FetchUserLogin(this.userModel);
    }
    
    // 用於清空錯誤資訊的 action
    class ClearUserLoginErrorMessage {}
    複製程式碼

    我們新增了上述的 4 個 action 來處理我們的業務場景。修改 redux_trigger 元件,並新增 TriggerViewModel 來關聯我們的元件和store:

    // screen/redux_trigger.dart
    class TriggerViewModel {
      final String nameErrorMessage;
      final String emailNameError;
      final String ageNameError;
      final bool loginFlag;
      final Function(UserModel) fetchUserLogin;
    
      TriggerViewModel({
        this.nameErrorMessage,
        this.emailNameError,
        this.ageNameError,
        this.loginFlag,
        this.fetchUserLogin
      });
    
      factory TriggerViewModel.create(Store<AppState> store) {
        _fetchUserLogin(UserModel userModel) {
          // store.dispatch(new ClearUserLoginErrorMessage());
          store.dispatch(new SetLoginFlag(loginFlag: true));
        }
    
        return TriggerViewModel(
          nameErrorMessage: store.state.userState.nameErrorMessage,
          emailNameError: store.state.userState.emailErrorMessage,
          ageNameError: store.state.userState.ageErrorMessage,
          loginFlag: store.state.globalState.loginFlag,
          fetchUserLogin: _fetchUserLogin
        );
      }
    }
    複製程式碼

    修改 redux_trigger build 方法,並在 UI 中增加錯誤提示元件:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

    ...
      model.emailNameError.isNotEmpty ? Text(model.emailNameError, style: textStyle) : Container(),
      TextFormField(
        decoration: InputDecoration(labelText: 'Age'),
        onSaved: (input) => userModel.age = input,
      ),
      model.ageNameError.isNotEmpty ? Text(model.ageNameError, style: textStyle) : Container(),
      FlatButton(
        onPressed: () {
          formKey.currentState.save();
          
          model.fetchUserLogin(userModel);

          // formKey.currentState.reset();
        },
        child: Text('遞交資訊'),
        color: Colors.blue,
        textColor: Colors.white,
      )
    ...
複製程式碼

接下來,我們在 store/ 目錄下,新增 middleware 檔案用於放置中介軟體,並新增 AuthorizationMiddleware 類用於登入鑑權相關業務的處理與 action 派發:

    // store/middlewares.dart
    class AuthorizationMiddleware extends MiddlewareClass<AppState> {
      void validateUserInfo(UserModel userModel, NextDispatcher next) {
        Map<String, String>  errorMessage = new Map<String, String>();
        if (userModel.name.isEmpty) {
          errorMessage['nameErrorMessage'] = '姓名不能為空';
        }
        if (userModel.email.length < 10) {
          errorMessage['emailErrorMessage'] = '郵箱格式不正確';
        }
        if (userModel.age.toString().isNotEmpty && int.parse(userModel.age) < 0) {
          errorMessage['ageErrorMessage'] = '年齡不能為負數';
        }
        if (errorMessage.isNotEmpty) {
          next(
            new LoginFormFieldError(
              errorMessage['nameErrorMessage'],
              errorMessage['emailErrorMessage'],
              errorMessage['ageErrorMessage'],
            )
          );
        } else {
            next(new SetLoginFlag(loginFlag: true));
        }
        
      }
    
      @override
      void call(Store<AppState> store, dynamic action, NextDispatcher next) {
          if (action is ValidateUserLoginFields) {
            validateUserInfo(action.userModel, next);
          }
      }
    }
複製程式碼

AuthorizationMiddleware 類,繼承了 MiddlewareClass, 我們重寫他的 call 方法,並在其中去做 action 動作的過濾,當傳送動作為 ValidateUserLoginFields 時,呼叫 validateUserInfo 方法對入參進行校驗。我們將對應的 action 傳遞到 Next 函式中,傳送給下一個中介軟體。

在 store/middlewares 下,管理相關的中介軟體:

    List<Middleware<AppState>> createMiddlewares() {
      return [
        AuthorizationMiddleware()
      ];
    }
複製程式碼

在 main.dart 中初始化中介軟體:

  final store = Store<AppState>(
    appReducer,
    middleware: createMiddlewares(),
    initialState: AppState(
      globalState: GlobalState.initState(),
      userState: UserState.initState(),
    )
  );
複製程式碼

前面我們提到了中介軟體通過 next 函式完成由 中介軟體 -> reducer -> 中介軟體 這個排程過程的,回頭看看 AuthorizationMiddleware 的方法你會發現當 action 動作並非是 ValidateUserLoginFields 時,AuthorizationMiddleware 並沒有將 action 繼續向後傳遞交給下一個中介軟體。這便導致了整個排程過程的停止,修改 call 方法:

    ....
    @override
        void call(Store<AppState> store, dynamic action, NextDispatcher next) {
          if (action is ValidateUserLoginFields) {
            validateUserInfo(action.userModel, next);
          }
          next(action)
        }
複製程式碼

可以看到這時的執行效果:

Flutter 記錄 - Flutter State Management (Redux)使用介紹

非同步 action 的呼叫

接下來,修改 AuthorizationMiddleware 中介軟體處理非同步問題:


/// 模擬 service 非同步請求
void fetchUserLogin(Store<AppState> store, UserModel userModel) async {
  UserModel model = await Future.delayed(
    Duration(milliseconds: 2000),
    () {
      return new UserModel(name: '服務返回name', age: 20, email: 'luma@qq.com');
    }
  );
  if (model != null) {
    store.dispatch(new SetUserInfo(model));
    store.dispatch(new SetLoginFlag(loginFlag: true));
  }
}


class AuthorizationMiddleware extends MiddlewareClass<AppState> {

  void validateUserInfo(Store<AppState> store, UserModel userModel, NextDispatcher next) {
    if (userModel.name.isEmpty) {
        ...
    } else {
      fetchUserLogin(store, userModel);
    }
    
  }
    ...
}

複製程式碼

當請求校驗通過後,便在 middleware 中呼叫 fetchUserLogin 方法請求後臺 api 介面,根據返回值處理使用者資訊了。

此時執行效果如下:

Flutter 記錄 - Flutter State Management (Redux)使用介紹
可以看到,再點選提交按鈕,等待2s之後,便登入成功並拿到後臺返回的資訊了。

使用 redux_thunk 處理非同步 action

把所有的業務放到中介軟體來做也不是唯一的選擇,有時候我們可能會在 viewModel 去處理各類校驗或業務,我們的 action 可能會包含一些副作用。如何處理帶副作用的 action 呢?我們可以藉助 redux_thunk 這個元件來處理非同步action

首先在 pubspec.yaml 引入 redux_thunk

修改 store/action.dart,新增一個非同步 action:

    
class ReduxThunkLoginFetch {
  static ThunkAction<AppState> fetchUserLogin(UserModel userModel) {
    return (Store<AppState> store) async {
      UserModel model = await Future.delayed(
        Duration(milliseconds: 2000),
        () {
          return new UserModel(name: '服務返回name', age: 20, email: 'luma@qq.com');
        }
      );
      if (model != null) {
        store.dispatch(new SetUserInfo(model));
        store.dispatch(new SetLoginFlag(loginFlag: true));
      }
    };
  }
}
複製程式碼

可以看到,我們在一個 ReduxThunkLoginFetch action 類中,增加了一個靜態方法,該方法處理了與之前 AuthorizationMiddleware 中一樣的方法,所不同的是,這個方法被標識為一個 ThunkAction , 因為他內部返回了 Future.

此時在 redux_trggier 中,便可以通過呼叫 ReduxThunkLoginFetch.fetchUserLogin 來獲取返回:

/// redux_trigger viewModel
_fetchLoginWithThunk(UserModel userModel) {
  // todo 校驗
  store.dispatch(ReduxThunkLoginFetch.fetchUserLogin(userModel));
}
複製程式碼

redux-thunk 中介軟體為我們攔截了 ThunkAction 型別的 action 動作。當派發動作是一個 ThunkAction 的時候,redux-thunk 會執行這個 action, 並傳遞 store 和響應的引數到 action 方法中完成非同步 action 的呼叫。

redux combineReducers


combineReducers 是一個高階函式,可以幫我們組合多個 reducer 函式。 並提供了對 reducer action 的一些校驗,實際場景中可根據需要使用。

redux state 是否 immutable ?


使用 redux 在對 state 狀態進行設計的時候,往往我們希望的是全域性只有一個 state 例項。就拿上面的示例來說,appState、userState、globalState 他們應該都是全域性唯一,不可改變的。在 Dart 中,我們可以通過對類新增裝飾器的模式,來標識我們的類是 immutable 的:

@immutable
class CountState {
  final bool loginFlag;

  GlobalState({
    @required this.loginFlag
  });
  
  GlobalState.initState(): loginFlag = false;
}
複製程式碼

dart 語法會自動檢測被裝飾的類,是否具有可變屬性(是否有 final 宣告)。

有關 immutable 可以檢視 immutable 介紹

當宣告一個類是 immutable 之後,便可在編譯階段對類的屬性進行檢測,並可防止其他人對 state 進行修改和混入了。

Flutter 記錄 - Flutter State Management (Redux)使用介紹

相關文章