purpose

  1. What is the story
  2. Redux’s role in Flutter
  3. How is Redux used in Flutter

The effect

The Flutter App is essentially a single page application that requires us to maintain our own State, Model and Route. As business grows, this can become complex and unpredictable, it can be difficult to reproduce a bug, and it can be difficult to pass data across components. The idea of Redux is inherited from Flux, which decouples business hierarchies through reasonable conventions, and the changes of data can be predicted and reproduced. Redux has three principles:

1. Single data source (unified App Store)

2. The State State is read-only (data cannot be modified directly, but can only be triggered by the agreed Action or Reduce)

3. Data changes must be pure functions (these pure functions, called Reducer, define how to modify the Store, triggered by actions)

The principle of

Redux (3.0.0) is the JS Redux library implemented by Dart. It defines Store, Action, Reduce, Middleware and their behavioral relationships.

Flutter_redux (0.5.2) is a tool class that Bridges Redux and Flutter. It provides StoreProvider, StoreBuilder, and StoreConnector components, making it easy to use Redux in Flutter.

The flow chart

Action

Action defines an Action that can carry information and send it to the Store. In other words, a Store change must be triggered by an Action. Live Template shortcut key AC to create a set of Api Aciton:

class xxxRequestAction extends VoidAction {}

class xxxSuccessAction extends ActionType {
  final  payload;
  xxxSuccessAction({this.payload}) : super(payload: payload);
}

class xxxFailureAction extends ActionType {
  final RequestFailureInfo errorInfo;
  xxxFailureAction({this.errorInfo}) : super(payload: errorInfo);
}
Copy the code

API

The minimum granularity of App functionality depends on API. Generally, we will agree on a set of Rest interface definitions at the front and back ends. Here APP side is implemented with a static method encapsulation, which defines the response callback of Path, Request, Success, Failure three actions.

 static fetchxxx() {
    final access = StoreContainer.access;
    final apiFuture = Services.rest.get(
 '/zpartner_api/${access.path}/${access.businessGroupUid}/xxxx/');
    Services.asyncRequest(
        apiFuture,
        xxxRequestAction(),
        (json) => xxxSuccessAction(payload: xxxInfo.fromJson(json)),
        (errorInfo) => xxxFailureAction(errorInfo: errorInfo));
 }

Copy the code

Reduce&state

State is a node of Store and defines the data declaration of this node. Each time Reduce responds to an Action, it creates a new State to replace the node State in the original Store. Reduce and State are basically one-to-one, so put them in one file. Live Template shortcut rd to create a set of Reduce&State:

@immutable
class xxxState {
  final bool isLoading;
 
  xxxState({this.isLoading});
  
  xxxState copyWith({bool isLoading}) {
    return xxxState(isLoading: isLoading ?? this.isLoading);
  }
  
  xxxState.initialState() : isLoading = false;
    
}

class xxxReducer {
  xxxState reducer(xxxState state, ActionType action) {
    switch (action.runtimeType) {
      case xxxRequestAction:
        return state.copyWith(isLoading: );
      case xxxSuccessAction:
        return state.copyWith(isLoading: );
      case xxxFailureAction:
        return state.copyWith(isLoading: );
        
      default: 
        returnstate; }}}Copy the code

Middleware

Middleware, which is inserted between Action triggers and does not reach Reduce, is generally used to make some API asynchronous requests and process them. This step was optional, at the time flutter_EPIC performance was not stable enough, given that the Dio network library had Json processing for data. So instead of Middleware, we wrapped a tool method in API Services that calls the processing API directly and distributes the corresponding Action based on the result. There is a need for access integration testing, and it is important to consider whether to introduce it.

The advanced

Global Action

The logout operation in the App is special, it can be invoked in different modules, and all you need to do is clear the entire Store. We used a GlobalReduce to distribute the Action

AppState reduxReducer(AppState state, action) =>
    GlobalReducer().reducer(state, action);

class GlobalReducer {
  AppState reducer(AppState state, ActionType action) {
    switch (action.runtimeType) {
      case AppRestartAction:
        hasToken();
        return _initialReduxState();
      default:
        returnAppState( login: LoginReducer().reducer(state.login, action), ...) }}}Copy the code

APIFuction

As mentioned earlier, instead of using Middleware, we packaged a tool called Function, which has the advantage of being easy to use and the disadvantage of writing tests without a clear return value.

  /// common function for network with dio
  /// Future<Response> apiFuture [Dio.request]
  /// request action
  /// success action
  /// failure action
  static asyncRequest(
    Future<Response> apiFuture,
    ActionType request,
    ActionType Function(dynamic) success,
    ActionType Function(RequestFailureInfo) failure,
  ) async {
    // request  
    StoreContainer.global.dispatch(request);
    final requestBegin = DateTimeUtil.dateTimeNowMilli();
    try {
      final response = await apiFuture;

      final requestEnd = DateTimeUtil.dateTimeNowMilli();
      final requestSpend = requestEnd - requestBegin;
      if (requestSpend < requestMinThreshold) {
        await Future.delayed(Duration(
            milliseconds:
                requestMinThreshold - requestSpend)); // The request returns too fast, the page is a little bit sluggish, a little embarrassing todo
      }
      // success 
      StoreContainer.global.dispatch(success(response.data));
    } on DioError catch (error) {
      var message = ' ';
      var code = '1';
      var url = ' ';
      if(error.response ! =null) {
        var errorData = error.response.data;
        List messageList = errorData is Map<String.dynamic>? ((errorData['message'])?? []) : []; messageList .forEach((item) => message = message + item.toString() +' ');
        code = error.response.statusCode.toString();
        url = error.response.request.baseUrl + error.response.request.path;
      } else {
        message = error.message;
      }
      final model = RequestFailureInfo(
          errorCode: code,
          errorMessage: message,
          dateTime: DateTimeUtil.dateTimeNowIso());
        // failureStoreContainer.global.dispatch(failure(model)); }}Copy the code

Local refresh

When the StoreConnector component provided by Flutter_redux is used, you can set distinct to ture. You can completely control whether the view is refreshed after the Store changes. The idea is to override the ViewModel == operator and override the HashCode method. When the Store changes, the StoreStreamListener triggers a builder by comparing whether the viewModels are equal to each other, which we override and control ourselves.

class _RestartAppViewModel {
  Key key;
  bool isLogin;

  _RestartAppViewModel({this.key, this.isLogin});

  static _RestartAppViewModel fromStore(Store<AppState> store) =>
      _RestartAppViewModel(
          key: store.state.cache.key, isLogin: store.state.cache.isLogin);

  @override
  int get hashCode => key.hashCode ^ isLogin.hashCode;

  @override
  bool operator ==(other) =>
      identical(this, other) ||
      other is _RestartAppViewModel &&
          key == other.key &&
          isLogin == other.isLogin;
}

StoreConnector<AppState, _RestartAppViewModel>(
          distinct: true,
          builder: (context, vm) {
            return App(vm.isLogin, vm.key);
          },
          converter: (store) => _RestartAppViewModel.fromStore(store))
  
Copy the code

The sample source code

Github.com/hyjfine/flu…

reference

Github.com/johnpryan/r…

(after)

@ Zi Luyu, the copyright of this article belongs to Zaihui RESEARCH and development team, welcome to reprint, please reserve the source.