“This is the fifth day of my participation in the First Challenge 2022. For details: First Challenge 2022”

In application development, network request is almost an essential function. This paper will introduce how to achieve network request encapsulation step by step through dio secondary encapsulation, so as to facilitate the use of network request in projects.

The encapsulated network request has the following functions:

  • Simple and easy to use
  • Data parsing
  • Exception handling
  • Request to intercept
  • Log print
  • Loading show

The following steps will take you through the encapsulation of network requests.

Add the dependent

First add the DIO dependency to the project:

dependencies:
  dio: ^ 4.0.4
Copy the code

Request to packaging

Create a RequestConfig class to place dio configuration parameters as follows:

class RequestConfig{
  static const baseUrl = "https://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test";
  static const connectTimeout = 15000;
  static const successCode = 200;
}
Copy the code

The baseUrl of the request, the connection timeout period, and the service code for the successful request are configured. If other configurations are required, you can configure them in this class.

Create a RequestClient to encapsulate the dio request and initialize the DIO configuration in the constructor of the class:

RequestClient requestClient = RequestClient();

class RequestClient {
  lateDio _dio; RequestClient() { _dio = Dio( BaseOptions(baseUrl: RequestConfig.baseUrl, connectTimeout: RequestConfig.connectTimeout) ); }}Copy the code

At the top of the class, a global variable requestClient is created for external calls.

Dio itself provides a series of HTTP request methods such as GET, POST, PUT and DELETE. However, through the source code, it is found that these methods are finally implemented by the method of the call request. So here we’re directly encapsulating Dio’s request method.

  Future<dynamic> request(
    String url, {
    String method = "GET".Map<String.dynamic>? queryParameters,
    data,
    Map<String.dynamic>? headers
  }) async{ Options options = Options() .. method = method .. headers = headers; Response response =await _dio.request(url,
        queryParameters: queryParameters, data: data, options: options);

    return response.data;
  }
Copy the code

Commonly used parameters are uniformly encapsulated as request method, and then REQUEST method of DIO is called, and unified data processing, such as data parsing, is carried out in request method.

Data parsing

Return data parsing

In mobile development, developers are used to parse returned data into entity classes. In the construction of Flutter application framework (iii)Json data parsing, this article explains how to parse Json data into entity classes in Flutter. Next, it will introduce how to complete the packaging of data parsing with DIO.

The typical data structure returned by an interface in project development looks like this:

{
  "code": 200."message": "success"."data": {"id": "12312312"."name": "loongwind"."age": 18}}Copy the code

Create the ApiResponse class to parse the data returned by the API interface.

class ApiResponse<T> {

	int? code;
	String? message;
	T? data;

  ApiResponse();

  factory ApiResponse.fromJson(Map<String.dynamic> json) => $ApiResponseFromJson<T>(json);

  Map<String.dynamic> toJson() => $ApiResponseToJson(this);

  @override
  String toString() {
    return jsonEncode(this); }}Copy the code

For details about JSON parsing, please read the Application framework of Flutter (3) JSON data parsing

The data type of the data in the request method is variable, so the request method is generic, and then the data is parsed in the request method, and then the data is returned. The code is as follows:

Future<T? > request<T>(String url, {
    String method = "GET".Map<String.dynamic>? queryParameters,
    data,
    Map<String.dynamic>? headers
  }) async{ Options options = Options() .. method = method .. headers = headers; Response response =await _dio.request(url,
        queryParameters: queryParameters, data: data, options: options);

    return _handleRequestResponse<T>(response);
  }

  ///Request response content processing
  T? _handleResponse<T>(Response response) {
    if (response.statusCode == 200) {
      ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data);
      return _handleBusinessResponse<T>(apiResponse);
    } else {
      return null; }}///Business content processing
  T? _handleBusinessResponse<T>(ApiResponse<T> response) {
    if (response.code == RequestConfig.successCode) {
      return response.data;
    } else {
      return null; }}Copy the code

Data is returned through the ApiResponse parsing, and then the business code of the ApiResponse is judged to be successful. If so, data data is returned.

Sometimes a third-party interface needs to be called in the application, but the data structure returned by the third-party interface may be different. In this case, the original data needs to be returned for separate processing. Create a RawData class that parses RawData:

class RawData{
  dynamic value;
}
Copy the code

Then modify _handleResponse in RequestClient:

// Request response content processing T? _handleResponse<T>(Response response) { if (response.statusCode == 200) { if(T.toString() == (RawData).toString()){ RawData raw = RawData(); raw.value = response.data; return raw as T; }else { ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data); return _handleBusinessResponse<T>(apiResponse); } } else { var exception = ApiException(response.statusCode, ApiException.unknownException); throw exception; }}Copy the code

New check whether the generic type is RawData, if yes, directly remove response.data and return it in RawData, that is, RawData value is the original data returned by the interface.

Request data conversion

In addition to parsing the returned data, the actual development process will also encounter the processing of the request parameters, such as the request parameters are JSON data, but in order to facilitate the processing of the entity class, the data parameter in the request may be passed an entity class instance. At this point, the data needs to be converted to JSON data before the data request.

  _convertRequestData(data) {
    if(data ! =null) {
      data = jsonDecode(jsonEncode(data));
    }
    returndata; } Future<T? > request<T>(String url, {
    String method = "GET".Map<String.dynamic>? queryParameters,
    data,
    Map<String.dynamic>? headers
  }) async {
		///.
    data = _convertRequestData(data);

    Response response = await _dio.request(url,
        queryParameters: queryParameters, data: data, options: options);

    return_handleResponse<T>(response); }}Copy the code

The _convertRequestData method is used here to convert the requested data data into a string using jsonEncode, and then into a Map using the jsonDecode method.

Exception handling

Let’s take a look at uniform exception handling. Exceptions are generally divided into two parts: Http exceptions and business exceptions.

  • Http exception: Http errors, such as 404 and 503
  • Service exception: The request is successful but the service is abnormal, for example, the user name or password is incorrect during login

First create an ApiException to encapsulate the requested exception information uniformly:

class ApiException implements Exception {
  static const unknownException = "Unknown error";
  final String? message;
  final int? code;
  String? stackInfo;

  ApiException([this.code, this.message]);

  factory ApiException.fromDioError(DioError error) {
    switch (error.type) {
      case DioErrorType.cancel:
        return BadRequestException(- 1."Request cancellation");
      case DioErrorType.connectTimeout:
        return BadRequestException(- 1."Connection timed out");
      case DioErrorType.sendTimeout:
        return BadRequestException(- 1."Request timed out");
      case DioErrorType.receiveTimeout:
        return BadRequestException(- 1."Response timeout");
      case DioErrorType.response:
        try {
          
          /// HTTP error code contains service error information
          ApiResponse apiResponse = ApiResponse.fromJson(error.response?.data);
          if(apiResponse.code ! =null) {return ApiException(apiResponse.code, apiResponse.message);
          }
          
          int?errCode = error.response? .statusCode;switch (errCode) {
            case 400:
              return BadRequestException(errCode, "Request syntax error");
            case 401:
              returnUnauthorisedException(errCode! ."No access");
            case 403:
              returnUnauthorisedException(errCode! ."Server refused to execute");
            case 404:
              returnUnauthorisedException(errCode! ."Unable to connect to server");
            case 405:
              returnUnauthorisedException(errCode! ."Request method prohibited");
            case 500:
              returnUnauthorisedException(errCode! ."Server internal error");
            case 502:
              returnUnauthorisedException(errCode! ."Invalid request");
            case 503:
              returnUnauthorisedException(errCode! ."Server exception");
            case 505:
              returnUnauthorisedException(errCode! ."HTTP request not supported");
            default:
              returnApiException( errCode, error.response? .statusMessage ??'Unknown error'); }}on Exception catch (e) {
          return ApiException(- 1, unknownException);
        }
      default:
        return ApiException(- 1, error.message); }}factory ApiException.from(dynamic exception){
    if(exception is DioError){
      return ApiException.fromDioError(exception);
    } if(exception is ApiException){
      return exception;
    } else {
      var apiException = ApiException(- 1, unknownException); apiException.stackInfo = exception? .toString();returnapiException; }}}/// Request error
class BadRequestException extends ApiException {
  BadRequestException([int? code, String? message]) : super(code, message);
}

/// Unauthenticated exception
class UnauthorisedException extends ApiException {
  UnauthorisedException([int code = - 1.String message = ' ') :super(code, message);
}
Copy the code

ApiException is created primarily from DioError information, but a closer look shows that there is a code that parses the returned data to create an ApiException, as follows:

ApiResponse apiResponse = ApiResponse.fromJson(error.response?.data);
if(apiResponse.code ! =null) {return ApiException(apiResponse.code, apiResponse.message);
}
Copy the code

The reason is that sometimes the returned HTTP status code is modified when the back-end service is abnormal. When the HTTP status code does not start with 200, Dio will throw DioError error, but the error information needed at this time is the error information in Response. Therefore, response data needs to be parsed to obtain error information.

After the ApiException class is created, we need to catch exceptions in the Request method. We modify the request method as follows:

Future<T? > request<T>(String url, {
    String method = "Get".Map<String.dynamic>? queryParameters,
    data,
    Map<String.dynamic>? headers,
    bool Function(ApiException)? onError,
  }) async {
    try{ Options options = Options() .. method = method .. headers = headers; data = _convertRequestData(data); Response response =await _dio.request(url,
          queryParameters: queryParameters, data: data, options: options);

      return _handleResponse<T>(response);
    } catch (e) {
      var exception = ApiException.from(e);
      if(onError? .call(exception) ! =true) {throwexception; }}return null;
  }

  ///Request response content processing
  T? _handleResponse<T>(Response response) {
    if (response.statusCode == 200) {
      ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data);
      return _handleBusinessResponse<T>(apiResponse);
    } else {
      var exception = ApiException(response.statusCode, ApiException.unknownException);
      throwexception; }}///Business content processing
  T? _handleBusinessResponse<T>(ApiResponse<T> response) {
    if (response.code == RequestConfig.successCode) {
      return response.data;
    } else {
      var exception = ApiException(response.code, response.message);
      throwexception; }}Copy the code

Add bool Function(ApiException) to request method? The onError parameter is used as a callback for error information processing and returns a bool.

Add a try-catch package to the request method and create ApiException in the catch. Call onError. When onError returns true, that is, the error message has been processed by the caller, no exception will be thrown.

At the same time, the method of parsing response data also adds the processing of throwing exceptions. When a service exception occurs, the corresponding service exception information is thrown.

After the above encapsulation, abnormal information can indeed be processed, but there is a problem in the actual development. In the development, other processing is often done after the interface request is successful, such as data processing or interface refresh, and a prompt or error processing is displayed after the request fails. According to the above encapsulation, it is necessary to determine whether the returned data is null and not empty for subsequent processing. If a business has multiple request dependent calls, it will be nested for several times, resulting in poor code reading. As follows:

var data1 = requestClient.request(url1);
if( data1 ! =null) {var data2 = requestClient.request(url2);
  if(data2 ! =null) {var data3 = requestClient.request(url3);
    ///.}}Copy the code

To solve the above problems and achieve uniform exception handling, create a top-level request method:

Future request(Function() block,  {bool Function(ApiException)? onError}) async{
  try {
    await block();
  } catch (e) {
    handleException(ApiException.from(e), onError: onError);
  }
  return;
}


bool handleException(ApiException exception, {bool Function(ApiException)? onError}){

  if(onError? .call(exception) ==true) {return true;
  }

  if(exception.code == 401) {///todo to login
    return true;
  }
  showError(exception.message ?? ApiException.unknownException);

  return false;
}
Copy the code

The request method takes a block function argument, which is called from the request and wrapped around a try-catch, which is handled uniformly in the catch, and handleException, which is handled uniformly in the external exception. If 401 is used, the login page is displayed, and an error message is displayed for other errors.

In this case, use the following:

  void testRequest() => request(() async {
    UserEntity? user = await apiService.test();
    print(user? .name); user =await apiService.test();
    print(user? .name); });Copy the code

If one of the requests in the request package code fails, it does not proceed.

Request to intercept

Dio supports adding interceptors to custom process requests and return data by implementing a custom Interceptor class that inherits the Interceptor and implements onRequest and onResponse.

For example, interceptors can be used to add unified headers carrying token information to all requests after login.

class TokenInterceptor extends Interceptor{

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    ///token from cache
    var token = Cache.getToken();
    options.headers["Authorization"] = "Basic $token";
    super.onRequest(options, handler);
  }

  @override
  void onResponse(dio.Response response, ResponseInterceptorHandler handler) {
    super.onResponse(response, handler); }}Copy the code

Then add interceptors when initializing DIO:

_dio.interceptors.add(TokenInterceptor());
Copy the code

Log print

In order to facilitate debugging in the development process often need to print the request return log, you can use a custom interceptor to achieve, you can also use a third-party implementation of log print interceptor prettY_dio_logger library.

Add dependencies:

pretty_dio_logger: ^ 1.1.1
Copy the code

Dio adds date blocker:

_dio.interceptors.add(PrettyDioLogger(requestHeader: true, requestBody: true, responseHeader: true));
Copy the code

The PrettyDioLogger interceptor sets what information to print, as required.

Print effect:

Winding foot Request ║ POST flutter: ║ HTTPS://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test/testFlutter: ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝ flutter: Techheaders flutter: carecontent content-type: application/json; charset=utf- 8 -Flutter: ╟ Authorization: Basic ZHhtaF93ZWI6ZHhtaF93ZWJfc2VjcmV0 flutter: ╟ token: Bearer flutter: ╟ contentType: application/json; charset=utf- 8 -Load responseType: responseType. Json flutter: trekkfollowredirects:trueFlutter: ╟ connectTimeout:15000Flutter: ╟ receiveTimeout:0Flutter: ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝ flutter, flutter: Techfoot Response ║ POST ║ Status:200 OK
flutter: ║  https://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test/testFlutter: ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝ flutter: Techheaders flutter: populationaccess-control-allow-credentials: [techheaders flutter: populationaccess-control-allow-credentials: [true[keep-alive] labelload (labelload) connection: labelload (labelload) x-powered-by: labelload (labelloadset- cookies: flutter: ║ [connect. Sid = % s3AkDiyUQw5crHmB0UuY03dYX3Z2HPVO8Sf.bOVO2aDh%2FSviB70e9Xt5sMQjkiDtorwn%2B%2F
flutter: ║ bKN7y8UtY; Path=/; Expires=Sun, 06 Feb 2022 21:37:08GMT; HttpOnly] flutter: labelage date: [Sun, cappelage]06 Feb 2022 09:37:08GMT] flutter: careall vary: [Accept, Origin, accept-encoding] careall content-length: [82[W/] flutter: carelabeletag"52-2tuUsqqRy8jX+vcUJL+3D5AmQss"] flutter: carecontent -type: [application/json; charset=utf- 8 -] flutter: careserver: [nginx/1.178.Flutter, ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝ flutter: Techbody flutter: ║ flutter: ║ {flutter: ║ code:200,
flutter: ║         message: "success",
flutter: ║         data: {id: 111111, name: zhangsan, age: 18} flutter: ║} flutter: ║ flutter: ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝Copy the code

Loading show

Network requests are time-consuming. In order to improve user experience, loading is displayed during the request, indicating that data is being loaded.

The loading method can be implemented in the same way:

Future loading( Function block, {bool isShowLoading = true}) async{
  if (isShowLoading) {
    showLoading();
  }
  try {
    await block();
  } catch (e) {
    rethrow;
  } finally {
    dismissLoading();
  }
  return;
}

void showLoading(){
  EasyLoading.show(status: "Loading...");
}

void dismissLoading(){
  EasyLoading.dismiss();
}
Copy the code

The implementation is simple. Loading’s show and dismiss are called before and after a block call. At the same time, the try-catch block package ensures that loading is cancelled when an exception occurs, and the exception is thrown directly without any processing in the catch.

Here loading uses the flutter_easyloading plug-in

Loading:

Future request(Function() block,  {bool showLoading = true.bool Function(ApiException)? onError, }) async{
  try {
    await loading(block, isShowLoading:  showLoading);
  } catch (e) {
    handleException(ApiException.from(e), onError: onError);
  }
  return;
}
Copy the code

Another layer of loading is wrapped for block in request to display and hide automatic loading.

Use the sample

Now that you’ve wrapped up the network request, let’s see how to use it.

The common network requests used in development are GET and POST. To facilitate the call, add get and POST methods to RequestClient as follows:

Future<T? >get<T>(
    String url, {
    Map<String.dynamic>? queryParameters,
    Map<String.dynamic>? headers,
    bool showLoading = true.bool Function(ApiException)? onError,
  }) {
    returnrequest(url, queryParameters: queryParameters, headers: headers, onError: onError); } Future<T? > post<T>(String url, {
    Map<String.dynamic>? queryParameters,
    data,
    Map<String.dynamic>? headers,
    bool showLoading = true.bool Function(ApiException)? onError,
  }) {
    return request(url,
        method: "POST",
        queryParameters: queryParameters,
        data: data,
        headers: headers,
        onError: onError);
  }
Copy the code

It’s actually a wrapped call to the Request method.

The basic use

  void login(String password) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = password;
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params);
    state.user = user;
    update();
  });


/// View
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    // Text("${SR.hello.tr} : ${state.count}", style: TextStyle(fontSize: 50.sp),),
    ElevatedButton(onPressed: () => controller.login("123456"), child: const Text("Normal Login")),
    ElevatedButton(onPressed: () => controller.login("654321"), child: const Text("Wrong login")),
    Text("Login user:${state.user? .username ??""}", style: TextStyle(fontSize: 20.sp),),

  ],
)
Copy the code

Custom exception handling

 void loginError(bool errorHandler) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = "654321";
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params);
    state.user = user;
   	print("-- -- -- -- -- -- -- -- -- -- -- -- --${user? .username ??"Login failed"}");
    update();
  }, onError: (e){
    state.errorMessage = "request error : ${e.message}";
    print(state.errorMessage);
    update();
    return errorHandler;
  });
Copy the code

The onError method is called whether onError returns false or true, and print(“————-${user? .username ?? “Login failed “); This output is not executed, and the error message is still displayed when onError returns false, because the default exception handling pop-up message is called when false is returned, and the default exception handling method is not called when true is returned.

Adding onError to a requestClient request method has the same effect, except that if onError is true on a requestClient, the following code will execute normally:

  void loginError(bool errorHandler) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = "654321";
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params,  onError: (e){
      state.errorMessage = "request error : ${e.message}";
      print(state.errorMessage);
      update();
      return errorHandler;
    });
    state.user = user;
    print("-- -- -- -- -- -- -- -- -- -- -- -- --${user? .username ??"Login failed"}");
    update();
  });
Copy the code

The interface is the same as above, when onError returns true, the code below requestClient will execute normally. The following code is not executed if false is returned and ————- login failed is printed.

Loading display hide

  void loginLoading(bool showLoading) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = "123456";
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params,  );
    state.user = user;
    update();
  }, showLoading: showLoading);
Copy the code

Switching the Interface Address

During the development process, there will be multiple environment addresses, such as development environment, test environment, pre-release environment, production environment, etc. In this case, it is common to add a function to change the environment during development. In this case, you can modify baseUrl and create a new RequestClient to implement. The code is as follows:

RequestConfig.baseUrl = "https://xxxxxx";
requestClient = RequestClient();
Copy the code

Source: flutter_app_core

The application framework of Flutter

  • GetX integration and Usage of Flutter application framework
  • Frame construction of Flutter application (2) Screen adaptation
  • (3) Json data analysis
  • Construct the application framework of Flutter (4) Network request encapsulation