Flutter 记录 - Flutter State Management (Redux)使用介绍

2,345 阅读16分钟

Flutter 及状态管理库简述


flutter 是由谷歌公司于 2015 年推出的移动 UI 框架。其选用 Dart 作为开发语言,内置 Material Design 和 Cupertino 两种设计风格 Widget 部件,使得它可以快速在 ios、android 设备上构建高效、高品质、高流畅度的用户 UI 界面。

Flutter 在很多设计理念上参考了 React 的设计理念,因此在状态管理库的选择和业务流处理上,也可以选择和 React 类似的解决方案,其中最具代表的便是 Redux。

为什么要使用状态管理库


在声明式 UI 组件开发的过程中,一个 Flutter 组件呈现出树状结构,抽象出来大概长这样:

可以发现,这种结构下,我们通过参数去传递一些数据,由父组件向子组件传递数据,子组件负责根据数据进行 UI 组件的渲染,这种数据流是自上而下流动的。

但是有时,我们会需要在 app 的不同界面中共享应用程序的某些状态或者是数据。如果他们没有任何关系,我们可能需要很多额外操作来做组件之间的通讯,传递这些数据。这时候,我们便需要一种独立在组件之后的数据源和状态库,来管理这些公共状态和数据。

应用状态的分类


在 Flutter 项目中,应用的状态分为短时状态应用状态:

  • 短时状态

短时状态也称局部状态。比如一个 tab 选项卡中,当前被选中的 tab 对应的序列号;一个输入框当前输入的值都可以称为短时状态(局部状态)。在一般场景下,对于这类状态,往往不是其他组件所关心的,也不需要我们帮助用户记住这种状态。即使应用重启,这些状态恢复到默认状态,也不会对应用造成太大影响,这种状态的丢失是可以接受的。

  • 应用状态

应用状态,也称共享状态,全局状态等。最具代表性的,便是 loginFlag(一个用于标识用户当前是否登录的字段)了。这种状态,往往影响了整个应用的逻辑和UI的渲染,因为用户是否登录决定我们是否返回当前用户的个人信息等。而且很多时候,登录状态一旦设置,我们可能在一段时间内要记住这种状态,即使应用重启,这种状态也不可丢失。类似这种的状态和数据,便称为应用状态。

状态管理库


在 Flutter 中,可提供应用状态管理的工具和第三方组件库有很多,如:Redux, Provider, BloC, RxDart 等。这次记录主要提供如下三种状态库的介绍及使用:

  • Redux 详细的使用介绍及编程规范
  • BloC 模式详细的使用
  • Provider 介绍及使用

我们通过使用 Redux,BloC 及 Provider 从分别完成一个数据流的传递,来对比这三者的使用。

需求概述


完成一个应用,通过提交用户信息来登录应用,记录下用户提交的信息,并展示。

实现的效果如下:

需求实现(Redux 版本)


  • 导入依赖

    使用 Redux ,我们需要先导入使用 Dart 编写的 Redux 状态库,还需要导入用于连接 Flutter 应用和Redux状态的链接库flutter-redux:

    在 pubspec.yml 文件中导入依赖, 并在命令行运行 flutter pub get 从远程服务器获取依赖:

  • 设计状态模型 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 仓库。该组件在输入完毕登录成功之后,处于影藏状态

    此时运行效果如下:

  • 在 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 组件

    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 实例

拿到实例以后,便可通过 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 接受两个泛型参数:

通过源码,可以看到第一个泛型,需要传入 Store 声明,第二参数可以传入你需要映射的类型。在 Flutter_redux 内部,converter 方法会在 initState 应用钩子初始化的时候调用:

有了这层转换,我们便可以去做逻辑层的抽象,将我们的实例映射成 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 中增加错误提示组件:

    ...
      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)
        }

可以看到这时的运行效果:

异步 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 接口,根据返回值处理用户信息了。

此时运行效果如下:

可以看到,再点击提交按钮,等待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 进行修改和混入了。