This is the 22nd day of my participation in the August More Text Challenge

preface

In the last article we introduced the basic concepts and simple examples of Redux’s application to Flutter, which didn’t seem that complicated. But these operations are all synchronous, clicking the button, initiating Action scheduling, then updating the status, and finally updating the interface are consistent. What if there is an asynchronous request, that is, we may click a button to initiate an asynchronous network request instead of an Action, and how do we notify status updates? Generally speaking, we need to initiate three actions in this scenario:

  • Network loading prompt: The interface notifies the user that data is currently being requested, please wait, usually oneLoadingPrompt.
  • Network request success: The backend data is received and the interface is notified to refresh the interface with the latest data status.
  • Network request failure: The interface notifies the user of a request failure, usually an error message.

There is no way to do this with a button click callback, so Redux’s middleware is needed. This article uses the contact list as an example to show how to use middleware to complete asynchronous network requests.

The preparatory work

  1. Start by updating the latest backend code:Back-end code (based on express.js)After the update, run under the directorynode seedContactor.jsGenerate databaseMockData, note that the picture here uses the local attachment address, the backend projectpublic/upload/imageBelow, if you need to show your contact profile picture, you can find a picture and modify itseedContactor.jsIn theavatarThe field is the file name of the corresponding picture. The interface address of the contact is:http://localhost:3900/api/contactor.
  2. Update dependency files, including the following plug-ins:
  • redux: ^5.0.0: Latest version of Redux plug-in
  • flutter_redux: ^0.8.2: Plugin for Flutter adaptation to Redux
  • dio: ^4.0.0: Network request plug-in, used to view the articles in this column:Summary of Dio chapter
  • flutter_easyrefresh: ^2.2.1: Pull up, pull down refresh component.
  • cached_network_image: ^3.1.0: Network image loading component that supports caching.
  • flutter_easyloading: ^3.0.0: Global popup alert component.
  • shared_preferences: ^2.0.6: Local offline simple key-value pair storage plug-in.
  1. Copy and initialize: Copy the network request utility class from the previous code to this project, complete as followsCookieManagerEasyLoadingInitialization of. Of course, you can download the code for this column’s Redux chapter directly here:Redux-based state management.

With that done, we are ready to roll out the code.

Redux SanBanFu

As mentioned earlier, one of the benefits of Redux is that the form of state management is unified, with three elementsAction,StoreReducerSo let’s start by sorting out the three elements of the contact list business.So let’s first defineAction, the list page will interact with 2Action, refresh and load more. But logically there are two more actions: get data on success and get data on failure, so there are fourAction.

  • Refresh: Gets the first page of data, defined asRefreshActionThat is used when interacting and scheduled when refreshing downAction.
  • Load more: Get data for the next page, defined asLoadActionWhich is called when using pull-up load time during interactionAction.
  • Loading success: The network request succeededSuccessAction.
  • Load failure: network request exception or errorFailedAction.

Success and failure are asynchronous operations without the possibility of active scheduling of user interactions. This is left to the main middleware of this article, which will be introduced separately later. The code for actions looks like this. Successes and failures have their own member attributes because they carry data to update the status:

class RefreshAction {}

class LoadAction {}

class SuccessAction {
  final List<dynamic> jsonItems;
  final int currentPage;

  SuccessAction(this.jsonItems, this.currentPage);
}

class FailedAction {
  final String errorMessage;

  FailedAction(this.errorMessage);
}
Copy the code

Next comes the Store state object, and we need to figure out what data we need. First of all, you need contact list data after a successful network request. Second is the page number of the current request, we need to request the next page data according to this page when loading more; Loading state markers and error messages are followed. Loading state markers can be used to prompt users in some situations, and error messages are used to remind them of errors. Therefore, the corresponding state data of Store is as follows:

  • contactors: Indicates the contact list dataList<dynamic>Type (to be withDioReceived data matches, only of this type).
  • isLoading: Indicates the loading identifier. The default isfalseWhen schedulingRefreshActionLoadAction“, marked astrue, when the request succeeds or failstrue, marked asfalse.
  • errorMessage: Error message, allowed to be null, so defined asString?.
  • currentPage: Page number of the current request, default is 1.

The code for the Store state object class is as follows:

class ContactorState {
  final List<dynamic> contactors;
  final isLoading;
  final String? errorMessage;
  final int currentPage;

  ContactorState(this.contactors,
      {this.isLoading = false.this.errorMessage, this.currentPage = 1});

  factory ContactorState.initial() => ContactorState(List.unmodifiable([]));
}

Copy the code

Finally, Reducer is defined as a function that returns a new state object based on the old state object and the current Action. The business logic is as follows:

  • RefreshActionProcessing: Flag the request statusisLoadingfortrue, the current page numbercurrentPagefor1, and other reservations are the same as the original state.
  • LoadActionProcessing: Flag the request statusisLoadingfortrue, the current page numbercurrentPageAdd for pages in the old state1, and other reservations are the same as the original state.
  • FailedActionHandling: Update status error message, marking request statusisLoadingforfalse.
  • SuccessActionProcessing: You need to decide how to update the list data based on the latest requested page number. If the current page number is1, replace the original list with the latest data. If the current page is greater than1, the new data is concatenated to the old data. After that, it updates the statusisLoadingforfalse, the page number is currentactionPage number, and emptyerrorMessage.

The Reducer code looks like this:


ContactorState contactorReducer(ContactorState state, dynamic action) {
  if (action is RefreshAction) {
    ContactorState contactorState = ContactorState(state.contactors,
        isLoading: true, errorMessage: null, currentPage: 1);
    return contactorState;
  }
  if (action is LoadAction) {
    ContactorState contactorState = ContactorState(state.contactors,
        isLoading: true,
        errorMessage: null,
        currentPage: state.currentPage + 1);
    return contactorState;
  }

  if (action is SuccessAction) {
    int currentPage = action.currentPage;
    List<dynamic> contactors = state.contactors;
    if (currentPage > 1) {
      contactors += action.jsonItems;
    } else {
      contactors = action.jsonItems;
    }
    ContactorState contactorState = ContactorState(contactors,
        isLoading: false, errorMessage: null, currentPage: currentPage);
    return contactorState;
  }

  if (action is FailedAction) {
    ContactorState contactorState = ContactorState(
      state.contactors,
      isLoading: false,
      errorMessage: action.errorMessage,
    );
    return contactorState;
  }

  return state;
}
Copy the code

The middleware

Middleware is similar to our previous Dio interceptor (see: Dio Interceptor) in that middleware methods are executed before Action is scheduled and then handed over to the next middleware. The interceptor for Redux is defined in the Store constructor in the form:

void (Store<T> store, action, NextDispatcher next)
Copy the code

Here, we define a middleware method called fetchContactorMiddleware that needs to be added to the Middleware parameter when building a Store object. Middleware itself is an array, so you can add multiple pieces of middleware for different processing.

final Store<ContactorState> store = Store(
  contactorReducer,
  initialState: ContactorState.initial(),
  middleware: [
    fetchContactorMiddleware,
  ],
);
Copy the code

In the middleware we can get the current Action and state, so we can do different business based on the Action. Here we just need to deal with refreshing and loading more:

  • When refreshing, set the page number to 1, request data, and initiate after the request is successfulSuccessActionScheduling, notifying status updates.
  • When more pages are loaded, add 1 to the page number and then request data. After the request is successful, it is initiatedSuccessActionScheduling, notifying status updates.
  • Requests are initiated if they failFailedActionScheduling, notifying status request failed.

After processing, remember to call the next method to pass the current action and generally complete the normal scheduling process. The code for the middleware is as follows:

void fetchContactorMiddleware(
    Store<ContactorState> store, action, NextDispatcher next) {
  const int pageSize = 10;
  if (action is RefreshAction) {
    // Refresh the first page of data
    ContactorService.list(1, pageSize).then((response) {
      if(response ! =null && response.statusCode == 200) {
        store.dispatch(SuccessAction(response.data, 1));
      } else {
        store.dispatch(FailedAction('Request failed'));
      }
    }).catchError((error, trace) {
      store.dispatch(FailedAction(error.toString()));
    });
  }

  if (action is LoadAction) {
    // Load more pages +1
    int currentPage = store.state.currentPage + 1;
    ContactorService.list(currentPage, pageSize).then((response) {
      if(response ! =null && response.statusCode == 200) {
        store.dispatch(SuccessAction(response.data, currentPage));
      } else {
        store.dispatch(FailedAction('Request failed'));
      }
    }).catchError((error, trace) {
      store.dispatch(FailedAction(error.toString()));
    });
  }

  next(action);
}
Copy the code

Page code

The page code is similar to the structure of the previous article, but this article builds a ViewModel class that uses StoreConnector’s Converter method to convert the list data in the state into the objects needed for page presentation.

class _ViewModel {
  final List<_ContactorViewModel> contactors;

  _ViewModel(this.contactors);

  factory _ViewModel.create(Store<ContactorState> store) {
    List<_ContactorViewModel> items = store.state.contactors
        .map((dynamic item) => _ContactorViewModel.fromJson(item))
        .toList();

    return_ViewModel(items); }}class _ContactorViewModel {
  final String followedUserId;
  final String nickname;
  final String avatar;
  final String description;

  _ContactorViewModel({
    required this.followedUserId,
    required this.nickname,
    required this.avatar,
    required this.description,
  });

  static _ContactorViewModel fromJson(Map<String.dynamic> json) {
    return _ContactorViewModel(
        followedUserId: json['followedUserId'],
        nickname: json['nickname'],
        avatar: UploadService.uploadBaseUrl + 'image/' + json['avatar'],
        description: json['description']); }}Copy the code

The build method of the page is as follows, and you can see that the middleware part of the code is not represented in the page, but is completed automatically during dispatch.

@override
Widget build(BuildContext context) {
  return StoreProvider<ContactorState>(
    store: store,
    child: Scaffold(
      / / omit appBar
      body: StoreConnector<ContactorState, _ViewModel>(
        converter: (Store<ContactorState> store) => _ViewModel.create(store),
        builder: (BuildContext context, _ViewModel viewModel) {
          return EasyRefresh(
            child: ListView.builder(
              itemBuilder: (context, index) {
                return ListTile(
                  leading:
                      _getRoundImage(viewModel.contactors[index].avatar, 50),
                  title: Text(viewModel.contactors[index].nickname),
                  subtitle: Text(
                    viewModel.contactors[index].description,
                    style: TextStyle(fontSize: 14.0, color: Colors.grey),
                  ),
                );
              },
              itemCount: viewModel.contactors.length,
            ),
            onRefresh: () async {
              store.dispatch(RefreshAction());
            },
            onLoad: () async {
              store.dispatch(LoadAction());
            },
            firstRefresh: true,); },),// omit other code));Copy the code

Note that the EasyRefresh component must be placed at the next level of StoreConnector, otherwise a null error will be reported because the ScrollView cannot be found during refresh.

The results

Run results as shown in the figure below, we use the Provider before the whole operation and do not have too big difference, but from the point of encapsulation, use Redux encapsulation will be better, such as part of the business on the middleware, network request for the component level only need to care about what to initiate action, and do not need to care about the specific actions after how to deal with. The code has been submitted to: Redux related code.

conclusion

Take a look at the process of Redux with middleware, as shown below.

After the addition of middleware, actions related to asynchronous operations can be triggered after the asynchronous result is completed, so that the state can be updated after the asynchronous result is processed by Reducer. With the introduction of middleware, asynchronous operations can be separated from interface interactions, further reducing coupling and improving code maintainability.


This is a column about the entry and practice of Flutter. The corresponding source code is here: Entry and Practice of Flutter.

👍🏻 : feel the harvest please point a praise to encourage!

🌟 : Collect articles, easy to look back!

💬 : Comment exchange, mutual progress!