Article series

A review of Flutter Dio

HttpClient, Http, Dio

Analysis of Flutter Dio source code (iii)- In-depth analysis

Analysis of Flutter Dio source code (iv)– Encapsulation

Video series

A review of The source code for Flutter Dio

A review of the source code for Flutter Dio

Analysis of Flutter Dio source code (iii)- In-depth analysis of the video tutorial

Analysis of The Source code of Flutter Dio (iv)– Encapsulating video tutorials

Source repository address

Github repository address

preface

This article will teach you how to encapsulate a class library by hand. Usually, we use the wheel made by others in our work. This article will show you how to build the wheel by yourself, and provide ideas and methods when we need to encapsulate other class libraries.

Why do YOU need to package Dio?

In the previous article, we have the basic use of Dio, request library comparison, source code analysis, we know that the use of Dio is very simple, so why do we need to package? There are two points:

1. Code migration

When a component library method changes significantly and needs to be migrated in multiple places, it needs to modify every file used, which can be tedious and problematic.

2. Request library switch

When the Dio library is not needed, we can easily switch to another network request library at any time, of course, Dio currently built-in support for the use of third-party library adapters.

3. Unified configuration

Because an application is almost uniformly configured, we can manage configurations for interceptors, converters, caches, unified error handling, proxy configuration, certificate verification, and more.

Use singleton mode for Dio encapsulation

Why use singleton pattern?

Since our application uses network requests on every page, instantiating a Dio every time we request it will simply add unnecessary overhead to the system, whereas using a singleton object will create the same object every time we access it, so we don’t need to instantiate the object again.

Create a singleton class

This is a singleton pattern created with a private constructor for static variables

class DioUtil {

  factory DioUtil() => _getInstance();
  static DioUtil get instance => _getInstance();
  static DioUtil _instance;

  DioUtil._init() {
    / / initialization
  }
  static DioUtil _getInstance() {
    if (_instance == null) {
      _instance = DioUtil._init();
    }
    return_instance; }}Copy the code

Initialize the Dio request

We set the timeout time, response time and BaseUrl uniformly

/// Connection timeout
static const int CONNECT_TIMEOUT = 60*1000;
/// Response timeout
static const int RECEIVE_TIMEOUT = 60*1000;

/// Declare the Dio variable
Dio _dio;

DioUtil._init() {
  if (_dio == null) {
    /// Initialize the base options
    BaseOptions options = BaseOptions(
      baseUrl: "http://localhost:8080",
      connectTimeout: CONNECT_TIMEOUT,
      receiveTimeout: RECEIVE_TIMEOUT
    );

    /// Initialize the dio_dio = Dio(options); }}Copy the code

Encapsulate Restful APi styles in a unified manner

Since both get() and POST () requests end up calling the Request method internally, just with a different method passed in, we define an enumerated type to handle in a method

enum DioMethod {
  get,
  post,
  put,
  delete,
  patch,
  head,
}

/// Request class
Future<T> request<T>(String path, {
  DioMethod method = DioMethod.get.Map<String.dynamic> params,
  data,
  CancelToken cancelToken,
  Options options,
  ProgressCallback onSendProgress,
  ProgressCallback onReceiveProgress,
}) async {
  const _methodValues = {
    DioMethod.get: 'get',
    DioMethod.post: 'post',
    DioMethod.put: 'put',
    DioMethod.delete: 'delete',
    DioMethod.patch: 'patch',
    DioMethod.head: 'head'}; options ?? = Options(method: _methodValues[method]);try {
    Response response;
    response = await _dio.request(path,
                                  data: data,
                                  queryParameters: params,
                                  cancelToken: cancelToken,
                                  options: options,
                                  onSendProgress: onSendProgress,
                                  onReceiveProgress: onReceiveProgress
                                 );
    return response.data;
  } on DioError catch (e) {
    throwe; }}Copy the code

The interceptor

introduce

We’ve simplified the Restful API style to a method that identifies different requests through DioMethod. In our normal development process, we need to do special processing on some interfaces before request, before response, when error, then we need to use interceptors. Dio provides us with a custom interceptor function, which is easy to implement request, response, error interception

Unified error handling

We found that although the Dio framework already packages a DioError class library, we can only customize it if we need to do uniform popover handling of returned errors or route jumps, etc

Unified processing before request

When we send a request, we will encounter several situations, such as the need to automatically add some specific parameters to the non-open interface, obtaining the request header to add a unified token

Unified processing before response

Before we request the interface, we can do some basic processing on the response data, such as custom encapsulation of the result of the response, and special processing for individual urls.

Custom interceptor implementation

import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_response.dart';

class DioInterceptors extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {

    // Add userId to all request parameters of non-open interfaces
    if(! options.path.contains("open")) {
      options.queryParameters["userId"] = "xxx";
    }

    // Add the token to the header
    options.headers["token"] = "xxx";

    // More business requirements

    handler.next(options);

    // super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {

    // Request success is basic processing of data
    if (response.statusCode == 200) {
      response.data = DioResponse(code: 0, message: "Request successful.", data: response);
    } else {
      response.data = DioResponse(code: 1, message: "Request failed", data: response);
    }

    // Do special processing for some individual URL return data
    if (response.requestOptions.baseUrl.contains("?????????")) {
      //....
    }

    // Customized according to the company's business needs

    / / the key
    handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    switch(err.type) {
        // The connection to the server timed out
      case DioErrorType.connectTimeout:
        {
          // Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
        }
        break;
        // Response timed out
      case DioErrorType.receiveTimeout:
        {
          // Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
        }
        break;
        // Send timed out
      case DioErrorType.sendTimeout:
        {
          // Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
        }
        break;
        // Request cancellation
      case DioErrorType.cancel:
        {
          // Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
        }
        break;
        / / 404/503 error
      case DioErrorType.response:
        {
          // Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
        }
        break;
        // other Other error type
      case DioErrorType.other:
        {

        }
        break;

    }
    super.onError(err, handler); }}class DioResponse<T> {

  /// Messages (such as success message text/error message text)
  final String message;
  /// Custom code(according to internal definition)
  final int code;
  /// The data returned by the interface
  final T data;
  /// Need to add more
  /// .

  DioResponse({
    this.message,
    this.data,
    this.code,
  });

  @override
  String toString() {
    StringBuffer sb = StringBuffer('{');
    sb.write("\"message\":\"$message\ "");
    sb.write(",\"errorMsg\":\"$code\ "");
    sb.write(",\"data\":\"$data\ "");
    sb.write('} ');
    returnsb.toString(); }}class DioResponseCode {
  /// successful
  static const int SUCCESS = 0;
  /// error
  static const int ERROR = 1;
  /// More and more
}
Copy the code

converter

introduce

Transformer is used to encode and decode request and response data. Dio implements a DefaultTransformer DefaultTransformer as the DefaultTransformer. If you want custom codec processing of request/response data, you can provide custom converters

Why do you need a converter?

We read the introduction of converters and found that the function of interceptors is similar to that of interceptors, so why there are converters, there are two points:

  1. Decoupled from interceptors
  2. Do not modify the original request data

Execution flow: Request Interceptor >> Request Converter >> Initiate a request >> Response Converter >> Response Interceptor >> End Result.

Request converter

Will only be used for ‘PUT’, ‘POST’, ‘PATCH’ methods because only these can carry the request body.

Response converter

Is used for the return data of all request methods.

Custom converter implementation

import 'dart:async';
import 'package:dio/dio.dart';

class DioTransformer extends DefaultTransformer {
  @override
  Future<String> transformRequest(RequestOptions options) async {
    // If the requested data interface is List
      
        then we throw an exception
      
    if (options.data is List<String>) {
      throw DioError(
        error: "You can't send List data directly to the server.",
        requestOptions: options,
      );
    } else {
      return super.transformRequest(options); }}@override
  Future transformResponse(RequestOptions options, ResponseBody response) async {
    For example, if we don't customize some header data in the response options, we can add it ourselves
    options.extra['myHeader'] = 'abc';
    return super.transformResponse(options, response); }}Copy the code

The refresh Token

In the development process, clients and servers often use a token for verification. Each company has a different logic for refreshing the token. Here is a simple example

We need to add a refreshToken to all the request headers. If the refreshToken does not exist, we first request the refreshToken, and then launch the subsequent requests after obtaining the refreshToken. Since the process of requesting refreshToken is asynchronous, we need to lock subsequent requests during the request (because they require refreshToken) until the refreshToken request is successful

import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_util.dart';

class DioTokenInterceptors extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (options.headers['refreshToken'] = =null) {
      DioUtil.instance.dio.lock();
      Dio _tokenDio = Dio();
      _tokenDio..get("http://localhost:8080/getRefreshToken").then((d) {
        options.headers['refreshToken'] = d.data['data'] ['token'];
        handler.next(options);
      }).catchError((error, stackTrace) {
        handler.reject(error, true);
      }) .whenComplete(() {
        DioUtil.instance.dio.unlock();
      }); // unlock the dio
    } else {
      options.headers['refreshToken'] = options.headers['refreshToken']; handler.next(options); }}@override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {

    // The token must be refreshed before the response

    super.onResponse(response, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    super.onError(err, handler); }}Copy the code

Cancel the request

Why do we need to have the function of canceling the request? If when our page sends the request, the user actively exits the current interface or the data does not respond when the APP exits, we need to cancel the network request to prevent unnecessary errors.

/// Cancel request token
CancelToken _cancelToken = CancelToken();

/// Canceling a network request
voidcancelRequests({CancelToken token}) { token ?? _cancelToken? .cancel("cancelled");
}
Copy the code

Cookie management

A cookie is introduced

A short piece of text information generated by the server is sent to the browser, and the browser saves the cookie in the form of KV in a text file under a local directory. The cookie will be sent to the server when the same website is requested next time.

The principle of

  1. The client sends a request (HTTP request + user authentication information) to the server
  2. After authentication is successful, the server sends an HttpResponse response to the client containing the set-cookie header
  3. The client extracts and saves the cookie to memory or disk
  4. When requested again, the HttpRequest request contains an authenticated Cookie header
  5. The server parses cookies to obtain information about clients in cookies
  6. The server returns the response data

use

The use of cookies requires two third-party components, dio_cookie_Manager and cookie_JAR

  • Cookie_jar:DarthttpThe request ofcookieManager, through which you can easily handle complex cookiePolicy and persistencecookie
  • Dio_cookie_manager: The CookieManager interceptor helps us automatically manage request/response cookies. CookieManager relies on the cookieJar package

The import file

dio_cookie_manager: ^2.0. 0
cookie_jar: ^3.01.
Copy the code
/// cookie
CookieJar cookieJar = CookieJar();

/// Add a cookie manager
_dio.interceptors.add(CookieManager(cookieJar));

List<Cookie> cookies = [
  Cookie("xxx", xxx),
  / /...
];

//Save cookies            
DioUtil.instance.cookieJar.saveFromResponse(Uri.parse(BaseUrl.url), cookies);

//Get cookies   
List<Cookie> cookies = DioUtil.instance.cookieJar.loadForRequest(Uri.parse(BaseUrl.url));
Copy the code

Network interface cache

Why cache?

In our normal development process, we would encounter a situation that when making a network request, we hope to access the last data normally, which is better for users’ experience, rather than displaying a blank page. The cache is mainly for reference of the network interface cache of Flutter.

Persistence using shared_preferences

The memory cache will disappear after the program exits, so we use shared_preferences for disk cache data.

import 'dart:collection';
import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_util.dart';

class CacheObject {
  CacheObject(this.response)
    : timeStamp = DateTime.now().millisecondsSinceEpoch;
  Response response;
  int timeStamp;

  @override
  bool operator ==(other) {
    return response.hashCode == other.hashCode;
  }

  @override
  int get hashCode => response.realUri.hashCode;
}

class DioCacheInterceptors extends Interceptor {
  // To ensure that the iterator order is consistent with the object insertion time order, we use LinkedHashMap
  var cache = LinkedHashMap<String, CacheObject>();

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if(! DioUtil.CACHE_ENABLE)return super.onRequest(options, handler);
    // Use the refresh field to determine whether to refresh the cache
    bool refresh = options.extra["refresh"] = =true;
    if (refresh) {
      // Delete the local cache
      delete(options.uri.toString());
    }
    // Caching is enabled only for GET requests
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == 'get') {
      String key = options.extra["cacheKey"]???? options.uri.toString();var ob = cache[key];
      if(ob ! =null) {
        // If the cache is not expired, the cache contents are returned
        if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
            DioUtil.MAX_CACHE_AGE) {
          return handler.resolve(cache[key].response);
        } else {
          // If it has expired, delete the cache and continue to request the servercache.remove(key); }}}super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // Save the response data to the cache
    if (DioUtil.CACHE_ENABLE) {
      _saveCache(response);
    }

    super.onResponse(response, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    // TODO: implement onError
    super.onError(err, handler);
  }


  _saveCache(Response object) {
    RequestOptions options = object.requestOptions;
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == "get") {
      // If the number of caches exceeds the maximum number, the earliest record is removed first
      if (cache.length == DioUtil.MAX_CACHE_COUNT) {
        cache.remove(cache[cache.keys.first]);
      }
      String key = options.extra["cacheKey"] ?? options.uri.toString();
      cache[key] = CacheObject(object);
    }
  }

  void delete(Stringkey) { cache.remove(key); }}Copy the code

Proxy configuration

Dio proxy needs to be configured when we use Flutter to capture packets. DefaultHttpClientAdapter provides an onHttpClientCreate callback to set up the proxy for the underlying HttpClient.

/// Setting the Http proxy (enable when set)
void setProxy({
  String proxyAddress,
  bool enable = false{})if (enable) {
    (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
      (HttpClient client) {
      client.findProxy = (uri) {
        return proxyAddress;
      };
      client.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true; }; }}Copy the code

Certificate of calibration

Used to verify that the site being visited is authentic. Security is provided because the certificate is bound to the domain name and is signed by the root certificate Authority.

/// Example Set HTTPS certificate verification
void setHttpsCertificateVerification({
  String pem,
  bool enable = false{})if (enable) {
    (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
      client.badCertificateCallback=(X509Certificate cert, String host, int port){
        if(cert.pem==pem){ // Verify the certificate
          return true;
        }
        return false; }; }; }}Copy the code

Unified Log Printing

Log printing is mainly to help us develop auxiliary troubleshooting

Void openLog() {_dio.interceptors.add(LogInterceptor(responseBody: true)); } DioUtil().openLog();Copy the code