Flutter network request framework Dio source code analysis and packaging (1)--request process analysis

foreword

It has been some time to use flutter to develop apps, and the most exposed code in this process is the code related to network requests. My current project uses the most popular network request library on the market - dio. Compared with the HttpClient that comes with flutter, dio is easier to use and more powerful. It supports global configuration, Restful API, FormData, and interceptors. , request cancellation, cookie management, file upload/download, timeouts, custom adapters, etc.

Purpose

The purpose of writing this article is to systematically understand the working principle of Dio. I encountered a problem of cookie persistence in network requests before, and it took a long time to solve it, wasting a lot of time. Only then did I discover that although Dio has been used for a long time, there are still many places where the underlying principles have not been understood. So, take this opportunity to sort out the source code of Dio from the beginning, and then repackage it based on the new understanding.

Request process - construct Dio object

First we need to import Dio's library, based on the 4.0.6 version used in the project

dependencies:
  dio: ^4.0.6 

First, let's start with a simple example given in the official documentation:

import 'package:dio/dio.dart';
final dio = Dio();
void getHttp() async {
    
    
  final response = await dio.get('https://dart.dev');
  print(response);
}

To use Dio to issue a get request, we first need to construct a Dio object instance. Let's look at the Dio class and its construction method:

abstract class Dio {
    
    
  factory Dio([BaseOptions? options]) => createDio(options);
  ...
  }

It can be seen that Dio is an abstract class, and Dio's construction method actually uses the factory-factory constructor (when using a factory to modify a constructor, DartVM will not always create a new object, but returns a memory object Objects that already exist in. For example, it may return an existing instance from the cache, or return an instance of a subclass), the implemented createDio is a factory method, and its implementation class is imported by platform distinction. Imported in the mobile terminal It is dio_for_native.dart, and createDio in this file creates a DioForNative object.

// ignore: uri_does_not_exist
    if (dart.library.html) 'entry/dio_for_browser.dart'
// ignore: uri_does_not_exist
    if (dart.library.io) 'entry/dio_for_native.dart';
Dio createDio([BaseOptions? baseOptions]) => DioForNative(baseOptions);

class DioForNative with DioMixin implements Dio {
    
    
  /// Create Dio instance with default [BaseOptions].
  /// It is recommended that an application use only the same DIO singleton.
  DioForNative([BaseOptions? baseOptions]) {
    
    
    options = baseOptions ?? BaseOptions();
    httpClientAdapter = DefaultHttpClientAdapter();
  }

It can be seen that in addition to implementing the abstract class Dio, DioForNative also mixes the DioMixin class. Through the principle of Mixins in Dart, the methods in DioMixin can be directly called. The methods such as get and post provided by Dio are mainly implemented in this class, which is understandable. Common logic for removing platform differences is implemented here.
There is an optional parameter baseOptions in the construction method, which is the basic configuration information of all network requests (each request can be configured separately to override this configuration, mainly including baseUrl, connectTimeout, receiveTimeout, etc.), and an httpClientAdapter, which is Dio The adapter with the HttpClient that comes with flutter finally calls HttpClient to initiate a request through httpClientAdapter. Let’s not delve into this first. Let’s look at the get method of dio:

Request Process - Constructing Request Parameters

  
  Future<Response<T>> get<T>(
    String path, {
    
    
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
    ProgressCallback? onReceiveProgress,
  }) {
    
    
    return request<T>(
      path,
      queryParameters: queryParameters,
      options: checkOptions('GET', options),
      onReceiveProgress: onReceiveProgress,
      cancelToken: cancelToken,
    );
  }

Get is implemented by DioMixin, and the request method is finally called, as are other methods such as post. Let's take a look at the incoming parameters:

  1. path: the requested url link
  2. data: Request data, such as FromData (post) used for uploading
  3. queryParameters: query parameters
  4. options: request configuration
  5. cancelToken: the token used to cancel the send request
  6. onSendProgress: The progress of network request sending
  7. onReceiveProgress: The progress of network request reception

In the request method, the compose method is mainly executed to merge the BaseOptions object input during initialization and the Options object passed in during calling into a RequestOptions object, and then call the fetch method and pass in the generated RequestOptions object.

    var requestOptions = options.compose(
      this.options,
      path,
      data: data,
      queryParameters: queryParameters,
      onReceiveProgress: onReceiveProgress,
      onSendProgress: onSendProgress,
      cancelToken: cancelToken,
    );
    ...
     return fetch<T>(requestOptions);

Request flow - build request flow and add interceptor

Construct an asynchronous request stream and loop through to add request interceptors to the request stream:

    // Start the request flow
    var future = Future<dynamic>(() => InterceptorState(requestOptions));

    // Add request interceptors to request flow
    interceptors.forEach((Interceptor interceptor) {
    
    
      var fun = interceptor is QueuedInterceptor
          ? interceptor._handleRequest
          : interceptor.onRequest;
      future = future.then(_requestInterceptorWrapper(fun));
    });

Interceptors are saved through queues, which are in "FIFO" mode, that is, the Interceptors added first will be processed first, and those added later will overwrite the previous processing. Usually, some headers and other operations will be added to onRequest, and the results will be processed in onResponse or onError. Handled in the way the caller wants, onResponse and onError are mutually exclusive

class Interceptor {
    
    

  // 发送请求前拦截  
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) =>
      handler.next(options);
  
  //在结果返回给调用者前拦截
  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) =>
      handler.next(response);
  
  //发生错误返回给调用者时拦截
  void onError(
    DioError err,
    ErrorInterceptorHandler handler,
  ) =>
      handler.next(err);
}

It can be seen that when traversing the interceptor, it will be judged whether it is of the type QueuedInterceptor, which can be understood as a serial mechanism. When multiple requests enter the interceptor at the same time, only one request is allowed to be executed first. It is often used when requesting a certain token, because other requests can be reused, and there is no need to repeat the request. I will not go into it for the time being. Under normal circumstances, execute onRequest directly:

  /// The callback will be executed before the request is initiated.
  ///
  /// If you want to continue the request, call [handler.next].
  ///
  /// If you want to complete the request with some custom data,
  /// you can resolve a [Response] object with [handler.resolve].
  ///
  /// If you want to complete the request with an error message,
  /// you can reject a [DioError] object with [handler.reject].
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) =>
      handler.next(options);

onRequest executes the next method of RequestInterceptorHandler, which is actually the complete method of a Completer object.

/// Handler for request interceptor.
class RequestInterceptorHandler extends _BaseHandler {
    
    
  /// Continue to call the next request interceptor.
  void next(RequestOptions requestOptions) {
    
    
    _completer.complete(InterceptorState<RequestOptions>(requestOptions));
    _processNextInQueue?.call();
  }
  ...
}
class _BaseHandler {
    
    
  final _completer = Completer<InterceptorState>();
  void Function()? _processNextInQueue;

  Future<InterceptorState> get future => _completer.future;

  bool get isCompleted => _completer.isCompleted;
}

Then look at the _requestInterceptorWrapper method:

    // Convert the request interceptor to a functional callback in which
    // we can handle the return value of interceptor callback.
    FutureOr Function(dynamic) _requestInterceptorWrapper(
      InterceptorSendCallback interceptor,
    ) {
    
    
      return (dynamic _state) async {
    
    
        var state = _state as InterceptorState;
        if (state.type == InterceptorResultType.next) {
    
    
          return listenCancelForAsyncTask(
            requestOptions.cancelToken,
            Future(() {
    
    
              return checkIfNeedEnqueue(interceptors.requestLock, () {
    
    
                var requestHandler = RequestInterceptorHandler();
                interceptor(state.data as RequestOptions, requestHandler);
                return requestHandler.future;
              });
            }),
          );
        } else {
    
    
          return state;
        }
      };
    }
typedef InterceptorSendCallback = void Function(
  RequestOptions options,
  RequestInterceptorHandler handler,
);

Here, the callback of the function is used as the parameter of the method, so that the interceptor is converted into a function callback, and a layer of judgment is made here. If the state.type is equal to next, then an asynchronous task listenCancelForAsyncTask that listens for cancellation will be added, and Pass the cancelToken to this task, then he will check whether the current interceptor request is enqueued, and finally define a request interceptor RequestInterceptorHandler, and assign it to the handler of InterceptorSendCallback, its future attribute, which is the _completer object The complete method is to execute the onRequest of the interceptor.

Request Process - Request Distribution

Then continue to operate on the request flow and add request distribution.

    // Add dispatching callback to request flow
    future = future.then(_requestInterceptorWrapper((
      RequestOptions reqOpt,
      RequestInterceptorHandler handler,
    ) {
    
    
      requestOptions = reqOpt;
      _dispatchRequest(reqOpt)
          .then((value) => handler.resolve(value, true))
          .catchError((e) {
    
    
        handler.reject(e as DioError, true);
      });
    }));
  // Initiate Http requests
  Future<Response<T>> _dispatchRequest<T>(RequestOptions reqOpt) async {
    
    
    var cancelToken = reqOpt.cancelToken;
    ResponseBody responseBody;
    try {
    
    
      var stream = await _transformData(reqOpt);
      responseBody = await httpClientAdapter.fetch(
        reqOpt,
        stream,
        cancelToken?.whenCancel,
      );
      responseBody.headers = responseBody.headers;
      var headers = Headers.fromMap(responseBody.headers);
      var ret = Response<T>(
        headers: headers,
        requestOptions: reqOpt,
        redirects: responseBody.redirects ?? [],
        isRedirect: responseBody.isRedirect,
        statusCode: responseBody.statusCode,
        statusMessage: responseBody.statusMessage,
        extra: responseBody.extra,
      );
      var statusOk = reqOpt.validateStatus(responseBody.statusCode);
      if (statusOk || reqOpt.receiveDataWhenStatusError == true) {
    
    
        var forceConvert = !(T == dynamic || T == String) &&
            !(reqOpt.responseType == ResponseType.bytes ||
                reqOpt.responseType == ResponseType.stream);
        String? contentType;
        if (forceConvert) {
    
    
          contentType = headers.value(Headers.contentTypeHeader);
          headers.set(Headers.contentTypeHeader, Headers.jsonContentType);
        }
        ret.data =
            (await transformer.transformResponse(reqOpt, responseBody)) as T?;
        if (forceConvert) {
    
    
          headers.set(Headers.contentTypeHeader, contentType);
        }
      } else {
    
    
        await responseBody.stream.listen(null).cancel();
      }
      checkCancelled(cancelToken);
      if (statusOk) {
    
    
        return checkIfNeedEnqueue(interceptors.responseLock, () => ret);
      } else {
    
    
        throw DioError(
          requestOptions: reqOpt,
          response: ret,
          error: 'Http status error [${
      
      responseBody.statusCode}]',
          type: DioErrorType.response,
        );
      }
    } catch (e) {
    
    
      throw assureDioError(e, reqOpt);
    }
  }

Initialize each Http request:

  1. _dispatchRequest will call _transfromData for data conversion, and the final converted data is a Stream.
  2. Call the httpClientAdapter to make the network request fetch method, and finally use the system request library HttpClient to make the network request.
  3. Convert the format of the response data, and finally return

Summarize

When we call methods such as get/post, we will enter the request method. The request method is mainly responsible for the unified processing of the request configuration parameters, and calls the fetch method, and fetch is the operation of constructing the request flow, adding interceptors, and request distribution . Next time we will follow this process and look at the principle of setting cookies.

Guess you like

Origin blog.csdn.net/Yaoobs/article/details/131168035
Recommended