Dart: an HttpClient provided by IO

1, support common Http operations, such as GET, POST, etc

Dart describes asynchronous operations

  • This library allows you to work with files, directories,
  • sockets, processes, HTTP servers and clients, and more.
  • Many operations related to input and output are asynchronous
  • and are handled using [Future]s or [Stream]s, both of which
  • are defined in the [dart:async
  • library](.. /dart-async/dart-async-library.html).
  • To use the dart:io library in your code:
  • import 'dart:io';
    Copy the code

3. Network invocation usually follows the following steps:

Create a client. Construct a Uri. Initiate a request and wait for the request. Close the request and wait for the response. Httpbin.org is a web site that tests HTTP requests and responses, such as cookie, IP, headers, and login authentication. It also supports GET, POST, and other methods.

_httpTest() async {
    var url = 'https://httpbin.org/ip';
    var httpClient = new HttpClient();
 
    String result;
    try {
      var request = await httpClient.getUrl(Uri.parse(url));
      var response = await request.close();
      if (response.statusCode == HttpStatus.ok) {
        var json = await response.transform(utf8.decoder).join();
        var data = jsonDecode(json);
        result = data['origin'];
        print(result);
      } else {
        result =
            'Error getting IP address:\nHttp status ${response.statusCode}'; }}catch (exception) {
      result = 'Failed getting IP address'; }} Output:122.70159.214.

Copy the code

Second, the dio

Dio is a powerful Dart Http request library with support for Restful apis, FormData, interceptors, request cancellation, Cookie management, file upload/download, timeouts, custom adapters, and more

1, install,

dependencies:

Dio: ^ 3.0.10

2. Common requests

The following experiment is based on Charles’ MapLocal json file containing {“animal”:”dog”}

– get

Response response;
Dio dio = _dio();
response = await dio.get("http://test? id=1&name=dio1&method=get");
 
// Request parameters can also be passed through objects.
response = await dio.get("http://test", queryParameters: {"id": 2."name": "dio2"});
 
print(response.data['animal']);
Copy the code

– post

Response response;
Dio dio = _dio();
response = await dio.post("http://test? id=1&name=dio1&method=post");
 
// Request parameters can also be passed through objects.
response = await dio.post("http://test", queryParameters: {"id": 2."name": "dio2"});
 
print(response.data['animal']);
 
// Monitor the progress of receiving data
response = await dio.post(
    "http://test",
    queryParameters: {"id": 2."name": "dio2"},
    onReceiveProgress: (int receive, int total) {
      print("$receive $total"); });Copy the code

– Initiate multiple requests

Dio dio = _dio();
Future.wait([dio.post("http://test/test1"), dio.get("http://test/test2")]).then((e) {
   print(e); }).catchError((e) {}); The results for [{"animal":"dog"}, {"animal":"dog"}]
Copy the code

– Download files

Dio dio = _dio();
Response response =
await dio.download("https://www.baidu.com/"."assets/data/test.html");
Copy the code

– Upload files

Response response;
Dio dio = _dio();
FormData formData;
formData = FormData.fromMap({
   "animal": "dog"}); response =await dio.post("http/test/upload", data: formData);
 
// Upload multiple files
formData = FormData.fromMap({
   "animal": "dog"."files": [
       await MultipartFile.fromFile("assets/data/test1.json", filename: "test1.json"),
       await MultipartFile.fromFile("assets/data/test2.json", filename: "test2.json")]}); response =await dio.post("http/test/upload", data: formData);
Copy the code

3. Configure the DIO

Dio dio = Dio();
// You can create a Dio instance using the default configuration or passing an optional BaseOptions parameter:
// Configure the dio instance
dio.options.baseUrl = "https://www.xx.com/api";
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;
 
// Or create dio instance by passing an 'options'
BaseOptions options = BaseOptions(
  baseUrl: "https://www.xx.com/api",
  connectTimeout: 5000,
  receiveTimeout: 3000,); dio = Dio(options);Copy the code

4. Request configuration

BaseOptions describes the common configuration of Dio instance to initiate network requests, while the Options class describes the configuration information of each Http request. Each request can be configured separately. The configuration information in the Options of a single request can overwrite the configuration in BaseOptions. Here are the BaseOptions configuration items: {/// Http method.
  String method;
 
  /// Request base address, can contain subpaths, such as: "https://www.google.com/api/".
  String baseUrl;
 
  /// Http request header.
  Map<String.dynamic> headers;
 
  /// Timeout for connecting to the server, in milliseconds.
  int connectTimeout;
  /// 2. X is the maximum time limit for receiving data.
  int receiveTimeout;
 
  /// Request path, if`path`Start with "HTTP (s)", then`baseURL`Will be ignored; Otherwise,
  /// The complete URL will be concatenated with baseUrl.
  String path = "";
 
  /// The content-type of the request. The default value is "application/json; charset=utf-8".
  /// If you want to encode request data in "Application/X-www-form-urlencoded" format,
  /// You can set this option to`Headers.formUrlEncodedContentType`So that (Dio)
  /// The request body is automatically encoded.
  String contentType;
 
  /// [responseType] indicates the format in which the response data is expected to be received.
  /// Currently [ResponseType] accepts three types`JSON`.`STREAM`.`PLAIN`.
  ///
  /// The default value is`JSON`When the content-Type in the response header is "Application/JSON", DIO automatically converts the response content into a JSON object.
  /// If you want to receive the response data in binary mode, such as downloading a binary file, you can use`STREAM`.
  ///
  /// If you want to receive response data in text (string) format, use the`PLAIN`.
  ResponseType responseType;
 
  /// `validateStatus`Determines whether the HTTP response status code is considered successful by DIO and returns`validateStatus`
  /// return`true`, the request result will be treated as success, otherwise it will be treated as failure.
  ValidateStatus validateStatus;
 
  /// User-defined fields are available in [Interceptor], [Transformer], and [Response].
  Map<String.dynamic> extra;
 
  /// Common query parameters
  Map<String.dynamic /*String|Iterable<String>*/ > queryParameters;
}
Copy the code

5. Response data

When the request succeeds, a Response object is returned containing the following fields: {/// Response data, which may have been converted, refer to [ResponseType] in Options for details.
  T data;
  /// Response headers
  Headers headers;
  /// This request information
  Options request;
  /// Http status code.
  int statusCode;
  /// Whether to redirect (Flutter Web is not available)
  bool isRedirect;
  /// Redirect information (Flutter Web is not available)
  List<RedirectInfo> redirects ;
  /// The actual requested URL (redirects the final URI)
  Uri realUri;
  /// The custom field of the response object (which can be set in the interceptor) that the caller can use in the`then`In the acquisition.
  Map<String.dynamic> extra;
}
Copy the code

6. Interceptors

We can implement custom interceptors by inheriting Interceptor

Each Dio instance can add as many interceptors as it wants, and they form a queue that executes in FIFO order. With interceptors you can do some uniform preprocessing before or after a request (but not yet processed by then or catchError). dio.interceptors.add(InterceptorsWrapper( onRequest:(RequestOptions options)async {
     Do something before the request is sent
     return options; //continue
     // If you want to complete the request and return some custom data, either return a 'Response' object or return 'dio.resolve(data)'.
     // The request will be terminated, the upper then will be called, and the data returned in the THEN will be your custom data.
     //
     // If you want to terminate the request and trigger an error, you can return a 'DioError' object, or 'dio.reject(errMsg)',
     // The request will be aborted and an exception will be raised and the upper catchError will be called.
    },
    onResponse:(Response response) async {
     // Do some preprocessing before returning response data
     return response; // continue
    },
    onError: (DioError e) async {
      // Do some preprocessing when the request fails
     return e;//continue}));Copy the code

7. Other asynchronous operations can be performed in interceptors

dio.interceptors.add(InterceptorsWrapper(
    onRequest:(Options options) async{
        / /... If no token, request token firstly.
        Response response = await dio.get("/token");
        //Set the token to headers
        options.headers["token"] = response.data["data"] ["token"];
        return options; //continue}));Copy the code

Lock/ UNLOCK interceptor

You can lock /unlock an interceptor by calling its lock()/unlock methods. Once the request/response interceptor is locked, subsequent requests/responses are queued before entering the request/response interceptor until unlocked. This is useful in some scenarios that require serialization of requests/responses, as we'll show you in an example below. tokenDio = Dio();//Create a instance to request the token.
tokenDio.options = dio.options;
dio.interceptors.add(InterceptorsWrapper(
    onRequest:(Options options) async {
        // If no token, request token firstly and lock this interceptor
        // to prevent other request enter this interceptor.
        dio.interceptors.requestLock.lock();
        // We use a Dio(to avoid dead lock) instance to request token.
        Response response = await tokenDio.get("/token");
        //Set the token to headers
        options.headers["token"] = response.data["data"] ["token"];
        dio.interceptors.requestLock.unlock();
        return options; //continue})); Consider this scenario: We need to set the token in each request header. If the token does not exist, we need to request the token first, obtain the token and then continue the request. Since the token request process is asynchronous, we need to lock the interceptor to prevent other requests from making network requests without obtaining the token, and unlock the token after obtaining the tokenCopy the code

9. Clear () to clear the wait queue

dio.interceptors.clear()
Copy the code

10. Log (Request and Response information will be printed after this function is enabled)

// Since the interceptor queue is executed in FIFO order, if you add a log interceptor to the front, the options changes made by subsequent interceptors will not be printed (but will still take effect), so it is recommended to add log interceptors to the back of the queue.
dio.interceptors.add(LogInterceptor(responseBody: false)); // Enable request logging
Copy the code

11, DioError

{
  /// Request info.
  RequestOptions request;
 
  /// Response info, it may be `null` if the request can't reach to
  /// the http server, for example, occurring a dns error, network is not available.
  Response response;
 
  /// Error type, see below
  DioErrorType type;
 
  ///The original Error or Exception object, usually present when type is DEFAULT.
  dynamic error;
}
 
enum DioErrorType {
  /// It occurs when url is opened timeout.
  CONNECT_TIMEOUT,
 
  /// It occurs when url is sent timeout.
  SEND_TIMEOUT,
 
  ///It occurs when receiving timeout.
  RECEIVE_TIMEOUT,
 
  /// When the server response, but with a incorrect status, such as 404, 503...
  RESPONSE,
 
  /// When the request is cancelled, dio will throw a error with this type.
  CANCEL,
   
  /// Default error type, Some other Error. In this case, you can
  /// read the DioError.error if it is not null.
  DEFAULT
}

Copy the code

CancelToken cancels the request

CancelToken token = CancelToken();
dio.post("/testpost? id=1&name=dio1&method=post",cancelToken: token).catchError((e) {
CancelToken CancelToken isCancel(err) is true
/ / such as
if (CancelToken.isCancel(err)) {
   print("Cancelled.");
}
 
 
}).then((data) {
  returndata; }); token.cancel(); Cancel_token. dart source code is also to judge DioErrorTypestatic bool isCancel(DioError e) {
  return e.type == DioErrorType.CANCEL;
}
Copy the code

Dio and HttpClient relationship

The HttpClientAdapter is a bridge between Dio and HttpClient. Abstract Adapter 2.0 to facilitate switching and customization of the underlying network library. Dio implements a standard, powerful API, and HttpClient is the object that actually initiates Http requests. We decouple Dio from HttpClient via HttpClientAdapter, so that we can customize the underlying implementation of Http requests. For example, we can use a custom HttpClientAdapter to forward Http requests to Native in Flutter. The request is then unified by Native. For example, if you want to make HTTP requests using OKHttp when the DART version of OKHttp is available one day, you can seamlessly switch to OKHttp through the adapter without changing the previous code. Dio uses DefaultHttpClientAdapter as its DefaultHttpClientAdapter, which uses dart: IO :HttpClient to initiate network requests.

Extension (Adapter mode) the first page defines the interface, the interface to implement the function to be abstracted, and then define different Adapter class to implement this interface, Adapter class is the docking port method of different implementations, the upper calling code does not need to change can switch to the underlying different function call.

14. Set the proxy

DefaultHttpClientAdapter provides an onHttpClientCreate callback to set the proxy of the underlying HttpClient. If we want to use the proxy, we can refer to the following code:import 'package:dio/dio.dart';
import 'package:dio/adapter.dart'; . (dio.httpClientAdapteras DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    // config the http client
    client.findProxy = (uri) {
        //proxy all request to localhost:8888
        return "PROXY localhost:8888";
    };
    // you can also create a HttpClient to dio
    // return HttpClient();
};
Copy the code

Part of the source code analysis

Dart network requests will eventually call the _request method when the Response generic class isStringIf the ResponseType is not bytes or stream, mergeOptions generates a RequestOptions object by combining the BaseOptions property of Dio with the request parameter Options. Options Future<Response<T>> _request<T>(String path, {
    data,
    Map<String.dynamic> queryParameters,
    CancelToken cancelToken,
    Options options,
    ProgressCallback onSendProgress,
    ProgressCallback onReceiveProgress,
  }) async {
    if (_closed) {
      throw DioError(error: "Dio can't establish new connection after closed."); } options ?? = Options();if (options is RequestOptions) {
      data = data ?? options.data;
      queryParameters = queryParameters ?? options.queryParameters;
      cancelToken = cancelToken ?? options.cancelToken;
      onSendProgress = onSendProgress ?? options.onSendProgress;
      onReceiveProgress = onReceiveProgress ?? options.onReceiveProgress;
    }
    var requestOptions = mergeOptions(options, path, data, queryParameters);
    requestOptions.onReceiveProgress = onReceiveProgress;
    requestOptions.onSendProgress = onSendProgress;
    requestOptions.cancelToken = cancelToken;
    if(T ! =dynamic &&
        !(requestOptions.responseType == ResponseType.bytes ||
            requestOptions.responseType == ResponseType.stream)) {
      if (T == String) {
        requestOptions.responseType = ResponseType.plain;
      } else{ requestOptions.responseType = ResponseType.json; }} The interceptor checks the checkIfNeedEnqueue method to see if there are any pending requests (this is done by the Completer), and if so, The Future. Then ((Callback)) returns a Future object, and the request is executed sequentially without interference with each other. The Callback in then is the second argument to the checkIfNeedEnqueue.// Convert the request/response interceptor to a functional callback in which
    // we can handle the return value of interceptor callback.
    Function _interceptorWrapper(interceptor, bool request) {
      return (data) async {
        var type = request ? (data is RequestOptions) : (data is Response);
        var lock =
            request ? interceptors.requestLock : interceptors.responseLock;
        if (_isErrorOrException(data) || type) {
          return listenCancelForAsyncTask(
            cancelToken,
            Future(() {
              return checkIfNeedEnqueue(lock, () {
                if (type) {
                  if(! request) data.request = data.request ?? requestOptions;return interceptor(data).then((e) => e ?? data);
                } else {
                  throwassureDioError(data, requestOptions); }}); })); }else {
          returnassureResponse(data, requestOptions); }}; }// If the interceptor is locked, subsequent request/response tasks are queued, otherwise execution continues
 
FutureOr checkIfNeedEnqueue(Lock lock, EnqueueCallback callback) {
    if (lock.locked) {
      return lock.enqueue(callback);
    } else {
      return callback();
    }
  }
 
 
  Future enqueue(EnqueueCallback callback) {
    if (locked) {
      // we use a future as a queue
      return _lock.then((d) => callback());
    }
    return null;
  }
 
 
// For real network requests, use httpClientAdapter
/ / request results will check after get interceptors. ResponseLock
// Will listen for cancellations
// Initiate Http requests
  Future<Response<T>> _dispatchRequest<T>(RequestOptions options) async {
    var cancelToken = options.cancelToken;
    ResponseBody responseBody;
    try {
      var stream = await _transformData(options);
      responseBody = awaithttpClientAdapter.fetch( options, stream, cancelToken? .whenCancel, ); responseBody.headers = responseBody.headers ?? {};var headers = Headers.fromMap(responseBody.headers ?? {});
      var ret = Response(
        headers: headers,
        request: options,
        redirects: responseBody.redirects ?? [],
        isRedirect: responseBody.isRedirect,
        statusCode: responseBody.statusCode,
        statusMessage: responseBody.statusMessage,
        extra: responseBody.extra,
      );
      var statusOk = options.validateStatus(responseBody.statusCode);
      if (statusOk || options.receiveDataWhenStatusError) {
        varforceConvert = ! (T ==dynamic || T == String) &&
            !(options.responseType == ResponseType.bytes ||
                options.responseType == ResponseType.stream);
        String contentType;
        if (forceConvert) {
          contentType = headers.value(Headers.contentTypeHeader);
          headers.set(Headers.contentTypeHeader, Headers.jsonContentType);
        }
        ret.data = await transformer.transformResponse(options, responseBody);
        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(
          response: ret,
          error: 'Http status error [${responseBody.statusCode}] ', type: DioErrorType.RESPONSE, ); }}catch (e) {
      throwassureDioError(e, options); }}/ / in io_adapter
// The default adapter in DIO is DefaultHttpClientAdapter, where HttpClient _defaultHttpClient, So dio network request and the underlying HttpClient is through DefaultHttpClientAdapter HttpClient network request implementation process
 
class DefaultHttpClientAdapter implements HttpClientAdapter {
  /// [Dio] will create HttpClient when it is needed.
  /// If [onHttpClientCreate] is provided, [Dio] will call
  /// it when a HttpClient created.
  OnHttpClientCreate onHttpClientCreate;
 
  HttpClient _defaultHttpClient;
 
  bool _closed = false;
 
  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<List<int>> requestStream,
    Future cancelFuture,
  ) async {
    if (_closed) {
      throw Exception(
          "Can't establish connection after [HttpClientAdapter] closed!");
    }
    var_httpClient = _configHttpClient(cancelFuture, options.connectTimeout); Future requestFuture = _httpClient.openUrl(options.method, options.uri); . (a)}Copy the code

16. Extension: Why do I need to set up the agent in code to use Charles?

Because when we start Charles we start an HTTP proxy server, tools like this tell the operating system, “Now I have created an HTTP proxy on my system with IP XXXXXX and port XX. When the HTTP client running on the system sends a request, it will not perform DNS resolution and connect to the target server, but directly connect to the system to tell it the address of the proxy. The proxy server then establishes a connection with the client, and the proxy server then connects to the real server based on the requested information. Dart in Flutter is described as follows

/ * *

  • Sets the function used to resolve the proxy server to be used for
  • opening a HTTP connection to the specified [url]. If this
  • function is not set, direct connections will always be used.
  • The string returned by [f] must be in the format used by browser
  • PAC (proxy auto-config) scripts. That is either
  • "DIRECT"
    Copy the code
  • for using a direct connection or
  • "PROXY host:port"
    Copy the code
  • for using the proxy server [:host:] on port [:port:].
  • A configuration can contain several configuration elements
  • separated by semicolons, e.g.
  • "PROXY host:port; PROXY host2:port2; DIRECT"
    Copy the code
  • The static function [findProxyFromEnvironment] on this class can
  • be used to implement proxy server resolving based on environment
  • variables.

*/ set findProxy(String f(Uri url));

// So if we do not set this up in code, we will request directly to the real server instead of the proxy server