Description of Flutter and state management library


Flutter is a mobile UI framework launched by Google in 2015. It uses Dart as its development language and has built-in Material Design and Cupertino Design style widgets, enabling it to quickly build efficient, high-quality, and fluent USER interfaces on ios and Android devices.

React has been used as a reference for many of the design concepts of Flutter. React can also be used as a solution for state management libraries and business flow processing. Redux is one of the best examples.

Why use a state management library


During declarative UI component development, a Flutter component takes on a tree structure that looks something like this when abstracted:

As you can see, in this structure, we pass some data through parameters, and the parent component passes the data to the child component, which is responsible for rendering the UI component based on the data. The data flow is top-down.

Sometimes, however, we need to share some state or data of the application between different interfaces of the app. If they don’t have any relationship, we might need a lot of extra work to communicate between components and pass this data. At this point, we need a data source and state library, independent of the component, to manage these common states and data.

Classification of application states


In the Flutter project, the application states are divided into short-term states and application states:

  • Briefly state

The short-term state is also called the local state. For example, in a TAB TAB, the sequence number of the TAB currently selected; Any value currently entered in an input box can be called a short-time state (local state). In a typical scenario, this kind of state is not something that other components care about, nor is it necessary for us to help users remember this state. Even if the application is restarted and these states are restored to the default state, the application is not affected too much, and the loss of state is acceptable.

  • Application state

Application status, also known as shared status, global status, etc. The best example of this is loginFlag, a field used to identify whether a user is currently logged in. This state often affects the logic of the entire application and the rendering of the UI, because whether the user logs in or not determines whether we return the current user’s personal information and so on. And many times, once the login status is set, we may have to remember it for a while, even if the application is restarted. Such states and data are called application states.

State management library


There are many tools and libraries of third-party components that provide application state management in Flutter, such as Redux, Provider, BloC, RxDart, etc. This record mainly provides the introduction and use of the following three state libraries:

  • Detailed introduction to the use of Redux and programming specifications
  • Detailed use of the BloC model
  • Provider describes and uses the Provider

We compared the use of Redux, BloC and Provider by transferring a data stream respectively.

Summary of demand


To complete an application, log in to the application by submitting user information, record the information submitted by the user, and display it.

The results are as follows:

Requirements Implementation (Redux version)


  • Import dependence

    To use Redux, we need to import the Redux state library written in Dart. We also need to import the Flutter library used to connect the Flutter application to the Redux state:

    Import dependencies in the pubspec.yml file and run flutter pub get on the command line to get dependencies from the remote server:

  • Design state Model Model

    Based on the requirements outlined above, our application state root AppState should contain at least two module states:

    * Global status => globleState Stores global application status ** * user status => userState Stores user-related application status **Copy the code


  • Generate the state UserState Model class

    According to the previous application state tree design, we first complete the establishment of the UserState Model class:

    Create a UserState Model class:

    /// 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
      });
    }
Copy the code

When using Redux for state management, it is often necessary to give some default values for the state of the application, so you can provide UserState with a constructor for initialization, initState, by naming the constructor:

    /// 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 = '[email protected]', age = 10;
    }
Copy the code

With the constructor, we can call the initState constructor where appropriate to provide default values for the UserState class.

Those who have used Redux know that in Redux, all states are generated by the Reducer pure function. The Reducer function merges new and old states and generates new states and returns them to the state tree. To prevent our last state from being lost, we should record the last state and merge it with the new state, so we also need to add a copy method to the UserState class for state merging:

For pure functions, see functional programming

    /// 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 = '[email protected]', age = 10;
      
      UserState copyWith(UserModel userModel) {
        return UserState(
          name: userModel.name ?? this.name,
          email: userModel.email ?? this.email,
          age: userModel.age ?? this.age ); }}Copy the code

We write a copyWith method in our class that takes user model information for the current instance and decides whether to return the old state by determining if a new value is passed in.

A UserState class is created.


  • Write the GlobalState, AppState Model classes

    Similar to UserState, we quickly completed the GlobalState, AppState class

    GlobalState model classes:

    /// 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 ); }}Copy the code

App State Model class:

    /// 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 ); }}Copy the code


  • Set up store warehouse

    Next, we need to create a store folder in the project root directory to store the actions and reducer files needed in the project:

    * - store
    *   - action.dart
    *   - reducer.dart
Copy the code
  • Write the action

    Based on the previous requirements, we will write the action action class in action that will be used in the project.

    // 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 {}
Copy the code

Generally, an Action consists of Type and Payload. Type identifies the Action Type, and Payload acts as the carrier of the function. Because of some features of the DART static language, classes are used as data carriers for easy scaling and logical processing of data. Using class name, string or enumeration to define your Action Action type determines how to determine the Action type in the Reducer and proceed with relevant logical processing. This operation can be flexibly handled based on service scenarios.

  • Compile the Reducer pure function

After defining the relevant action actions, we compiled the corresponding Reducer function. As mentioned earlier, the Reducer function accepts new and old states and merges them, generating new states and returning them to the state tree:

    // 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 {
        returnglobalState; }}Copy the code

In the above code, two pure functions, userSetUpReducer and globalStatusReducer, are defined. Their logic is very simple. By determining the action action type, merge the corresponding State to generate a new State and return it.

Since ‘classes’ were used as actions to be distributed, the class types could be determined by IS when the corresponding actions were processed in the Reducer

  • Write the top-level appReducer function

    After compiling the reducer function of the submodule, we need to complete the appReducer of the top-level function of the component state tree. AppReducer maintains the top-level state of our application. Here we give the corresponding module state to their Reducer function for processing:

    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),
      );
    }
Copy the code
The appReducer function, which accepts AppState, submits the userState and globalState states to their corresponding Reducer functions for processing using the copyWith method.Copy the code
  • Associate stores with your application

    In general, we only maintain a global store at the top level of the business, and the top level store accepts the reducer function to merge and distribute the state

    Next, we initialize the Store repository at the entrance to the app and bind it to the app:

    // main.dart.import 'package:flutter_redux/flutter_redux.dart';
    import 'package:redux/redux.dart';
    
    // before
    void main() {  
        runApp(MyApp())
    };
    
    // after
    void main() {
      finalstore = Store<AppState>( appReducer, initialState: AppState( globalState: GlobalState.initState(), userState: UserState.initState(), ) ); runApp( StoreProvider<AppState>( store: store, child: MyApp(), ) ); }...Copy the code

In the code above, we initialize a store repository via store in Redux, and in initialState we set the initialState of the application.

Then we associated store with application (MyApp) through StoreProvider method in Flutter_redux.

This completes the import of our Store repository.

  • Build UI test components

    Create a new redux_PerView component where you can edit the view:

    // 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),
          ]
        ),
      }
    }
    Copy the code

    Perview is responsible for displaying the user’s information. When the user is not logged in, this component hides and uses the store default value.

    Create a new redux_Trigger component and bind it to the UI for user input:

    // 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)
        } 
    }
    Copy the code

    The Trigger component accepts information entered by the user and submits it to the Store repository. The component is in the shadow state after the login is successful

    The running effect is as follows:

  • Use state in the ReduxPerviewPage component

    Next, we bind the state in the repository in the UI component.

    Flutter_redux provides two function components, StoreConnector and StoreBuilder. The usage scenarios of Flutter_redux will be further described at the end of this article.

    Here we use StoreBuilder to bind the PerView display page:

    To prevent too much nesting, separate the UI part into the _perviewWidget method

    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('Please log in'))); }}Copy the code

    In the above code, we use StoreBuilder’s Builder method to get the state of the context and store’s repository. The binding of the page Store state to the UI data is done by passing the state into the _perviewWidget using logical judgment.

  • Change the page state in ReduxTrggier

    Next we submit the action information in the Trigger component to change the state in state:

    The trigger component

    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();
                      // Submit the action
                      StoreProvider.of<AppState>(context).dispatch(new SetLoginFlag(loginFlag: true));
                      StoreProvider.of<AppState>(context).dispatch(new SetUserInfo(userModel));
                      
                      formKey.currentState.reset();
                    },
                    child: Text('Submit information'),
                    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) ); }}Copy the code

In the code above, we use the form form in the StatelessWidget Widget stateless component to assign a value to the instantiated userModel object. Using the StoreProvider class provided by Flutter_redux, you can retrieve the store instance by calling the static of method

After receiving the instance, the corresponding actions can be sent through the Dispatch method. After redux receives the actions, the reducer function will process them.

At this point, the introduction of the Redux business flow is complete.

StoreProvider is implemented by implementing the InheritedWidget mechanism, which works like context in Redux. When store changes, StoreConnector or StoreBuilder state gets the latest state. At this point, components wrapped through StoreConnector or StoreBuilder are updated.


Optimized code using StoreConnector


Flutter_redux provides two Builder components: StoreConnector and StoreBuilder. The two components are basically the same in implementation principle. When used in business, we should choose different link components according to different business scenarios to decouple our application to the maximum extent.

The redux_perView example above, refactored with 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('Please log in'))); }}Copy the code

StoreConnector calls the Converter function to map the Store to appState. With StoreConnector, we can process store parameters and map them to the states required by the component for use by the component.

StoreConnector accepts two generic parameters:

From the source code, you can see that the first generic is passed in the Store declaration, and the second parameter is passed in the type you need to map. Within Flutter_redux, the Converter method is called when initState applies hook initialization:

With this layer of transformation, we can abstract the logical layer and map our instance to the viewModel, thus further abstracting the logical layer. In the previous redux_perView example, we did the following:

Create a new PerviewViewModel class:

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());
    }

    returnPerviewViewModel( userModel: store.state.userState, loginFlag: store.state.globalState.loginFlag, clearUserInfo: _clearUserInfo, logout: _logout ); }}Copy the code

In the previewModelView, we pass in the store instance through the constructor create, and we pull all the store and UI-related business out into the viewModel. Modify the Converter method and change the mapping type to 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('Please log in'))); }...Copy the code

At this point, we change the UI component data passed in to the PerviewViewModel instance and modify the _perviewWidget UI component code:

. 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,
        )
...
Copy the code

As you can see, we can decouple the UI components from the combination of store state and logic code by separating it from the business in the form of the viewModel.

This model can effectively help us to unbind the UI and Store layers when dealing with some complex business processes. Separating the logic from the UI not only helps us keep the UI components simple, but also makes it easier to unit test the business with the viewModel logic. This will become clearer in future business modifications and migrations.

StoreConnector or StoreBuilder


As you can see from the above example, using StoreConnector can effectively decouple the business, and in some simple scenarios, using StoreConnector can lead to increased code volume. Therefore, when using StoreConnector or StoreBuilder, I think in some simple scenarios, we should extract UI components as much as possible, and have some control over the design and quantity of states. Then StoreBuilder can be used to directly deal with store-related logic.

However, for some complex business scenarios that require frequent store operations, StoreConnector can be used to abstract the business layer for future component reuse and code clarity, which is of great benefit to future maintenance.

Use of redux middleware


During business development, we can process our business logic in the viemModel. But for some asynchronous problems, such as calls to the Service API, where do you do it? Redux solves the problem of asynchronous Action invocation based on a generic design that includes middleware.

  • What exactly is middleware?

    Middleware is the entity that is responsible for the business side effects and handles the (often asynchronous) work associated with business logic. All actions pass through the middleware before reaching the Reducer function. Each middleware calls the next middleware through the next function, and passes the action action in the next, and then completes the scheduling process by the middleware => Reducer => middleware.

  • Middleware Usage Examples

    We combine two scenarios to demonstrate the use of middleware.

    Previously in the Redux_Trggier component, we set the login state by directly triggering the setLoginFlag action. In fact, in a real business scenario, we should verify the input parameter in the setUserInfo action before sending it to the server for authentication. After receiving the convention status code returned from the background through HTTP request, the user can determine whether the login is successful according to the status code and trigger the corresponding action

    For this business scenario, we use middleware to solve it.

    First, add some new actions with action distribution and related services:

    // store/action.dart
    
    // The user loads the page entry action
    class ValidateUserLoginFields {
      final UserModel userModel;
    
      ValidateUserLoginFields(this.userModel);
    }
    
    // The user loads the action with error information
    class LoginFormFieldError {
      final String nameErrorMessage;
      final String emailErrorMessage;
      final String ageErrorMessage;
    
      LoginFormFieldError(
        this.nameErrorMessage, 
        this.emailErrorMessage, 
        this.ageErrorMessage
      );
    }
    
    // Action used to send user information
    class FetchUserLogin {
      final UserModel userModel;
    
      FetchUserLogin(this.userModel);
    }
    
    // Action for clearing error messages
    class ClearUserLoginErrorMessage {}
    Copy the code

    We added the above four actions to handle our business scenario. Modify the redux_Trigger component and add TriggerViewModel to associate our component with the 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));
        }
    
        returnTriggerViewModel( nameErrorMessage: store.state.userState.nameErrorMessage, emailNameError: store.state.userState.emailErrorMessage, ageNameError: store.state.userState.ageErrorMessage, loginFlag: store.state.globalState.loginFlag, fetchUserLogin: _fetchUserLogin ); }}Copy the code

    Modify the redux_trigger build method and add an error component to the 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('Submit information'),
        color: Colors.blue,
        textColor: Colors.white,
      )
    ...
Copy the code

Next, in the Store/directory, we add a new Middleware file for middleware and an AuthorizationMiddleware class for login authentication related business processing and action distribution:

    // 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'] = 'Name cannot be empty';
        }
        if (userModel.email.length < 10) {
          errorMessage['emailErrorMessage'] = 'Email format is not correct';
        }
        if (userModel.age.toString().isNotEmpty && int.parse(userModel.age) < 0) {
          errorMessage['ageErrorMessage'] = 'Age cannot be negative';
        }
        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 isValidateUserLoginFields) { validateUserInfo(action.userModel, next); }}}Copy the code

The AuthorizationMiddleware class, which inherits MiddlewareClass, overwrites its call method to filter action actions. When sending ValidateUserLoginFields, Call the validateUserInfo method to validate the input parameter. We pass the corresponding action to the Next function and send it to the Next middleware.

Under Store/Middlewares, manage related middleware:

    List<Middleware<AppState>> createMiddlewares() {
      return [
        AuthorizationMiddleware()
      ];
    }
Copy the code

Initialize middleware in main.dart:

  final store = Store<AppState>(
    appReducer,
    middleware: createMiddlewares(),
    initialState: AppState(
      globalState: GlobalState.initState(),
      userState: UserState.initState(),
    )
  );
Copy the code

We mentioned above that the middleware completes the scheduling process by the middleware -> reducer -> through the next function. If you look back at the AuthorizationMiddleware method, you’ll see that when the action action is not ValidateUserLoginFields, AuthorizationMiddleware does not pass actions backward to the next middleware. This causes the entire scheduling process to stop, modifying the call method:

.@override
        void call(Store<AppState> store, dynamic action, NextDispatcher next) {
          if (action is ValidateUserLoginFields) {
            validateUserInfo(action.userModel, next);
          }
          next(action)
        }
Copy the code

You can see how this works:

Asynchronous action invocation

Next, modify the AuthorizationMiddleware to handle the asynchronous problem:


/// simulate an asynchronous service request
void fetchUserLogin(Store<AppState> store, UserModel userModel) async {
  UserModel model = await Future.delayed(
    Duration(milliseconds: 2000),
    () {
      return new UserModel(name: 'Service return name', age: 20, email: '[email protected]'); });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); }}... }Copy the code

When the request is verified, the fetchUserLogin method is called in Middleware to request the background API to process the user information based on the returned value.

The running effect is as follows:

Use redux_thunk to handle asynchronous actions

Putting all of our business into middleware is not the only option. Sometimes we may have to do all kinds of validation or business in the viewModel, and our actions may have side effects. What about actions with side effects? We can use the redux_thunk component to handle asynchronous actions

First introduce redux_thunk in pubspec.yaml

Dart/store/action.dart:

    
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: 'Service return name', age: 20, email: '[email protected]'); });if(model ! =null) {
        store.dispatch(new SetUserInfo(model));
        store.dispatch(new SetLoginFlag(loginFlag: true)); }}; }}Copy the code

As you can see, we’ve added a static method to a ReduxThunkLoginFetch Action class that handles the same methods as in AuthorizationMiddleware, except that, This method is identified as a ThunkAction because it internally returns the Future.

At this point in the redux_trggier, can by calling ReduxThunkLoginFetch. FetchUserLogin for return:

/// redux_trigger viewModel
_fetchLoginWithThunk(UserModel userModel) {
  / / todo
  store.dispatch(ReduxThunkLoginFetch.fetchUserLogin(userModel));
}
Copy the code

The Redux-Thunk middleware intercepts action actions of the ThunkAction type for us. When the dispatch action is a ThunkAction, redux-thunk executes the action, passing store and response parameters to the action method to complete the asynchronous action call.

redux combineReducers


CombineReducers is a high order function that can help us combine multiple reducer functions. It also provides some verification on reducer actions, which can be used as required in actual scenarios.

Is redux state immutable?


When designing state with Redux, it is often desirable to have only one instance of state globally. As in the example above, appState, userState, and globalState should all be globally unique and immutable. In Dart, we can indicate that our class is immutable by adding a decorator pattern to it:

@immutable
class CountState {
  final bool loginFlag;

  GlobalState({
    @required this.loginFlag
  });
  
  GlobalState.initState(): loginFlag = false;
}
Copy the code

The DART syntax automatically detects whether the class being decorated has mutable properties (or final declarations).

For details about immutable, see immutable

Declaring a class to be immutable allows the class’s properties to be checked at compile time and prevents others from altering and mixing in state.