Dio

Http请求-Dio package - 图7 Http请求-Dio package - 图8 Http请求-Dio package - 图9

通过上一节介绍,我们可以发现直接使用HttpClient发起网络请求是比较麻烦的,很多事情得我们手动处理,如果再涉及到

  • 文件上传/下载
  • Cookie管理等

就会非常繁琐。幸运的是,Dart社区有一些第三方http请求库,用它们来发起http请求将会简单的多,本节我们介绍一下目前人气较高的dio

  • dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等…
  • dio的使用方式随着其版本升级可能会发生变化,如果本节所述内容和dio官方有差异,请以dio官方文档为准
  • 以下版本为English,点击查看中文简体

添加依赖

  1. dependencies:
  2. dio: 3.0.9 #// 请使用pub上3.0.0分支的最新版本 latest version

dio 3.0.0为了支持Flutter Web,需要进行较大重构,因此无法直接兼容2.1.x, 如果你是2.1.x的用户,可以参照此文档升级到3.0,详情请查看 从2.1升级到3.0指南

一个极简的示例

  1. import 'package:dio/dio.dart';
  2. void getHttp() async {
  3. try {
  4. Response response = await Dio().get("http://www.google.com");
  5. print(response);
  6. } catch (e) {
  7. print(e);
  8. }
  9. }

相关插件

🎉 A curated list of awesome things related to dio.

Plugins Status Description
dio_cookie_manager Http请求-Dio package - 图10 A cookie manager for Dio
dio_http2_adapter Http请求-Dio package - 图11 A Dio HttpClientAdapter which support Http/2.0
dio_flutter_transformer Http请求-Dio package - 图12 A Dio transformer especially for flutter, by which the json decoding will be in background with compute function.
dio_http_cache Http请求-Dio package - 图13 A cache library for Dio, like Rxcache in Android. dio-http-cache uses sqflite as disk cache, and LRU strategy as memory cache.
retrofit Http请求-Dio package - 图14 retrofit.dart is an dio client generator using source_gen and inspired by Chopper and Retrofit.

Welcome to submit Dio’s third-party plugins and related libraries here

Examples

发起一个 GET 请求

  1. Response response;
  2. Dio dio = new Dio();
  3. response = await dio.get("/test?id=12&name=wendu");
  4. print(response.data.toString());
  5. // Optionally the request above could also be done as
  6. response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
  7. print(response.data.toString());

发起一个 POST 请求

  1. response = await dio.post("/test", data: {"id": 12, "name": "wendu"});

发起多个并发请求

  1. response = await Future.wait([dio.post("/info"), dio.get("/token")]);

下载文件

  1. response = await dio.download("https://www.google.com/", "./xx.html");

以流的方式接收响应数据

  1. Response<ResponseBody> rs = await Dio().get<ResponseBody>(url,
  2. options: Options(responseType: ResponseType.stream), //设置接收类型为stream
  3. );
  4. print(rs.data.stream); //response stream

以二进制数组的方式接收响应数据:

  1. Response<List<int>> rs = await Dio().get<List<int>>(url,
  2. options: Options(responseType: ResponseType.bytes), // // set responseType to `bytes`
  3. );
  4. print(rs.data); // List<int>

发送 FormData

  1. FormData formData = new FormData.fromMap({
  2. "name": "wendux",
  3. "age": 25,
  4. });
  5. response = await dio.post("/info", data: formData);

通过FormData上传多个文件

  1. FormData formData = FormData.fromMap({
  2. "name": "wendux",
  3. "age": 25,
  4. "file": await MultipartFile.fromFile("./text.txt",filename: "upload.txt"),
  5. "files": [
  6. await MultipartFile.fromFile("./text1.txt", filename: "text1.txt"),
  7. await MultipartFile.fromFile("./text2.txt", filename: "text2.txt"),
  8. ]
  9. });
  10. response = await dio.post("/info", data: formData);

监听发送(上传)数据进度

  1. response = await dio.post(
  2. "http://www.dtworkroom.com/doris/1/2.0.0/test",
  3. data: {"aa": "bb" * 22},
  4. onSendProgress: (int sent, int total) {
  5. print("$sent $total");
  6. },
  7. );

以流的形式提交二进制数据

  1. // Binary data
  2. List<int> postData = <int>[...];
  3. await dio.post(
  4. url,
  5. data: Stream.fromIterable(postData.map((e) => [e])), //create a Stream<List<int>>
  6. options: Options(
  7. headers: {
  8. Headers.contentLengthHeader: postData.length, // set content-length
  9. },
  10. ),
  11. );

Dio APIs

创建一个Dio实例,并配置它

建议在项目中使用Dio单例,这样便可对同一个dio实例发起的所有请求进行一些统一的配置,比如设置公共header、请求基地址、超时时间等;这里有一个在Flutter工程中使用Dio单例(定义为top level变量)的示例供开发者参考。

你可以使用默认配置或传递一个可选 BaseOptions参数来创建一个Dio实例 :

  1. Dio dio = new Dio(); // with default Options
  2. // Set default configs
  3. dio.options.baseUrl = "https://www.xx.com/api";
  4. dio.options.connectTimeout = 5000; //5s
  5. dio.options.receiveTimeout = 3000;
  6. // or new Dio with a BaseOptions instance.
  7. BaseOptions options = new BaseOptions(
  8. baseUrl: "https://www.xx.com/api",
  9. connectTimeout: 5000,
  10. receiveTimeout: 3000,
  11. );
  12. Dio dio = new Dio(options);

Dio实例的核心API

  1. Future request(String path,
  2. {data,
  3. Map queryParameters,
  4. Options options,
  5. CancelToken cancelToken,
  6. ProgressCallback onSendProgress,
  7. ProgressCallback onReceiveProgress})
  1. response=await request(
  2. "/test",
  3. data: {"id":12,"name":"xx"},
  4. options: Options(method:"GET"),
  5. );

请求方法别名

为了方便使用,Dio提供了一些其它的Restful API, 这些API都是request的别名

  1. Future get(...)
  2. Future post(...)
  3. Future put(...)
  4. Future delete(...)
  5. Future head(...)
  6. Future put(...)
  7. Future path(...)
  8. Future download(...)

请求配置

BaseOptions描述的是Dio实例发起网络请求的的公共配置,而Options类描述了每一个Http请求的配置信息,每一次请求都可以单独配置,单次请求的Options中的配置信息可以覆盖BaseOptions中的配置,下面是BaseOptions的配置项:

  1. {
  2. /// Http method.
  3. String method;
  4. /// 请求基地址,可以包含子路径,如: "https://www.google.com/api/".
  5. String baseUrl;
  6. /// Http请求头.
  7. Map<String, dynamic> headers;
  8. /// 连接服务器超时时间,单位是毫秒.
  9. int connectTimeout;
  10. /// 2.x中为接收数据的最长时限.
  11. int receiveTimeout;
  12. /// 请求路径,如果 `path` 以 "http(s)"开始, 则 `baseURL` 会被忽略; 否则,
  13. /// 将会和baseUrl拼接出完整的的url.
  14. String path = "";
  15. /// 请求的Content-Type,默认值是"application/json; charset=utf-8".
  16. /// 如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
  17. /// 可以设置此选项为 `Headers.formUrlEncodedContentType`, 这样[Dio]
  18. /// 就会自动编码请求体.
  19. String contentType;
  20. /// [responseType] 表示期望以那种格式(方式)接受响应数据。
  21. /// 目前 [ResponseType] 接受三种类型 `JSON`, `STREAM`, `PLAIN`.
  22. ///
  23. /// 默认值是 `JSON`, 当响应头中content-type为"application/json"时,dio 会自动将响应内容转化为json对象。
  24. /// 如果想以二进制方式接受响应数据,如下载一个二进制文件,那么可以使用 `STREAM`.
  25. ///
  26. /// 如果想以文本(字符串)格式接收响应数据,请使用 `PLAIN`.
  27. ResponseType responseType;
  28. /// `validateStatus` 决定http响应状态码是否被dio视为请求成功, 返回`validateStatus`
  29. /// 返回`true` , 请求结果就会按成功处理,否则会按失败处理.
  30. ValidateStatus validateStatus;
  31. /// 用户自定义字段,可以在 [Interceptor]、[Transformer] 和 [Response] 中取到.
  32. Map<String, dynamic> extra;
  33. /// Common query parameters
  34. Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;
  35. }

这里有一个完成的示例.

  1. import 'dart:io';
  2. import 'package:dio/dio.dart';
  3. main() async {
  4. Response response;
  5. var dio = Dio(BaseOptions(
  6. baseUrl: "http://httpbin.org/",
  7. connectTimeout: 5000,
  8. receiveTimeout: 100000,
  9. // 5s
  10. headers: {
  11. HttpHeaders.userAgentHeader: "dio",
  12. "api": "1.0.0",
  13. },
  14. contentType: Headers.jsonContentType,
  15. // Transform the response data to a String encoded with UTF8.
  16. // The default value is [ResponseType.JSON].
  17. responseType: ResponseType.plain,
  18. ));
  19. response = await dio.get("/get");
  20. print(response.data);
  21. Response<Map> responseMap = await dio.get(
  22. "/get",
  23. // Transform response data to Json Map
  24. options: Options(responseType: ResponseType.json),
  25. );
  26. print(responseMap.data);
  27. response = await dio.post(
  28. "/post",
  29. data: {
  30. "id": 8,
  31. "info": {"name": "wendux", "age": 25}
  32. },
  33. // Send data with "application/x-www-form-urlencoded" format
  34. options: Options(
  35. contentType: Headers.formUrlEncodedContentType,
  36. ),
  37. );
  38. print(response.data);
  39. response = await dio.request(
  40. "/",
  41. options: RequestOptions(baseUrl: "https://baidu.com"),
  42. );
  43. print(response.data);
  44. }

响应数据

当请求成功时会返回一个Response对象,它包含如下字段:

  1. {
  2. /// 响应数据,可能已经被转换了类型, 详情请参考Options中的[ResponseType].
  3. T data;
  4. /// 响应头
  5. Headers headers;
  6. /// 本次请求信息
  7. Options request;
  8. /// Http status code.
  9. int statusCode;
  10. /// 是否重定向(Flutter Web不可用)
  11. bool isRedirect;
  12. /// 重定向信息(Flutter Web不可用)
  13. List<RedirectInfo> redirects ;
  14. /// 真正请求的url(重定向最终的uri)
  15. Uri realUri;
  16. /// 响应对象的自定义字段(可以在拦截器中设置它),调用方可以在`then`中获取.
  17. Map<String, dynamic> extra;
  18. }

When request is succeed, you will receive the response as follows:

  1. Response response = await dio.get("https://www.google.com");
  2. print(response.data);
  3. print(response.headers);
  4. print(response.request);
  5. print(response.statusCode);

拦截器

每个 Dio 实例都可以添加任意多个拦截器,他们组成一个队列,拦截器队列的执行顺序是FIFO。通过拦截器你可以在请求之前或响应之后(但还没有被 then 或 catchError处理)做一些统一的预处理操作。

  1. dio.interceptors.add(InterceptorsWrapper(
  2. onRequest:(RequestOptions options) async {
  3. // 在请求被发送之前做一些事情
  4. return options; //continue
  5. // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。
  6. // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data.
  7. //
  8. // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`,
  9. // 这样请求将被中止并触发异常,上层catchError会被调用。
  10. },
  11. onResponse:(Response response) async {
  12. // 在返回响应数据之前做一些预处理
  13. return response; // continue
  14. },
  15. onError: (DioError e) async {
  16. // 当请求失败时做一些预处理
  17. return e;//continue
  18. }
  19. ));

一个简单的自定义拦截器示例:

  1. import 'package:dio/dio.dart';
  2. class CustomInterceptors extends InterceptorsWrapper {
  3. @override
  4. Future onRequest(RequestOptions options) {
  5. print("REQUEST[${options?.method}] => PATH: ${options?.path}");
  6. return super.onRequest(options);
  7. }
  8. @override
  9. Future onResponse(Response response) {
  10. print("RESPONSE[${response?.statusCode}] => PATH: ${response?.request?.path}");
  11. return super.onResponse(response);
  12. }
  13. @override
  14. Future onError(DioError err) {
  15. print("ERROR[${err?.response?.statusCode}] => PATH: ${err?.request?.path}");
  16. return super.onError(err);
  17. }
  18. }

完成和终止请求/响应

在所有拦截器中,你都可以改变请求执行流, 如果你想完成请求/响应并返回自定义数据,你可以返回一个 Response 对象或返回 dio.resolve(data)的结果。 如果你想终止(触发一个错误,上层catchError会被调用)一个请求/响应,那么可以返回一个DioError 对象或返回 dio.reject(errMsg)的结果.

  1. import 'dart:io';
  2. import 'package:dio/dio.dart';
  3. main() async {
  4. Dio dio = new Dio();
  5. dio.interceptors.add(InterceptorsWrapper(
  6. onRequest: (RequestOptions options) {
  7. if (options.path == '/vip') {
  8. return dio.resolve("暂时不支持【/vip】的请求");
  9. }
  10. },
  11. ));
  12. Response response = await dio.get("/vip");
  13. print(response.data); //"fake data"
  14. }

拦截器中支持异步任务

拦截器中不仅支持同步任务,而且也支持异步任务, 下面是在请求拦截器中发起异步任务的一个实例:

  1. dio.interceptors.add(InterceptorsWrapper(
  2. onRequest:(Options options) async{
  3. //...If no token, request token firstly.
  4. Response response = await dio.get("/token");
  5. //Set the token to headers
  6. options.headers["token"] = response.data["data"]["token"];
  7. return options; //continue
  8. }
  9. ));

Lock/unlock 拦截器

你可以通过调用拦截器的 lock()/unlock方法来锁定/解锁拦截器。一旦请求/响应拦截器被锁定,接下来的请求/响应将会在进入请求/响应拦截器之前排队等待,直到解锁后,这些入队的请求才会继续执行(进入拦截器)。这在一些需要串行化请求/响应的场景中非常实用,后面我们将给出一个示例。

  1. tokenDio = new Dio(); //Create a new instance to request the token.
  2. tokenDio.options = dio;
  3. dio.interceptors.add(InterceptorsWrapper(
  4. onRequest:(Options options) async {
  5. // If no token, request token firstly and lock this interceptor
  6. // to prevent other request enter this interceptor.
  7. dio.interceptors.requestLock.lock();
  8. // We use a new Dio(to avoid dead lock) instance to request token.
  9. Response response = await tokenDio.get("/token");
  10. //Set the token to headers
  11. options.headers["token"] = response.data["data"]["token"];
  12. dio.interceptors.requestLock.unlock();
  13. return options; //continue
  14. }
  15. ));

You can clean the waiting queue by calling ;

Clear()

你也可以调用拦截器的clear()方法来清空等待队列

别名

当请求拦截器被锁定时,接下来的请求将会暂停,这等价于锁住了dio实例,因此,Dio示例上提供了请求拦截器lock/unlock的别名方法:

  1. dio.lock() == dio.interceptors.requestLock.lock()
  2. dio.unlock() == dio.interceptors.requestLock.unlock()
  3. dio.clear() == dio.interceptors.requestLock.clear()

Example

假设这么一个场景:出于安全原因,我们需要给所有的请求头中添加一个csrfToken,如果csrfToken不存在,我们先去请求csrfToken,获取到csrfToken后,再发起后续请求。 由于请求csrfToken的过程是异步的,我们需要在请求过程中锁定后续请求(因为它们需要csrfToken), 直到csrfToken请求成功后,再解锁,代码如下:

  1. dio.interceptors.add(InterceptorsWrapper(
  2. onRequest: (Options options) async {
  3. print('send request:path:${options.path},baseURL:${options.baseUrl}');
  4. if (csrfToken == null) {
  5. print("no token,request token firstly...");
  6. //lock the dio.
  7. dio.lock();
  8. return tokenDio.get("/token").then((d) {
  9. options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
  10. print("request token succeed, value: " + d.data['data']['token']);
  11. print(
  12. 'continue to perform request:path:${options.path},baseURL:${options.path}');
  13. return options;
  14. }).whenComplete(() => dio.unlock()); // unlock the dio
  15. } else {
  16. options.headers["csrfToken"] = csrfToken;
  17. return options;
  18. }
  19. }
  20. ));

完整的示例在下面,或者代码请点击 这里.

  1. import 'dart:async';
  2. import 'package:dio/dio.dart';
  3. main() async {
  4. Dio dio = Dio();
  5. // dio instance to request token
  6. Dio tokenDio = Dio();
  7. String csrfToken;
  8. dio.options.baseUrl = "http://www.dtworkroom.com/doris/1/2.0.0/";
  9. tokenDio.options = dio.options;
  10. dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
  11. print('send request:path:${options.path},baseURL:${options.baseUrl}');
  12. if (csrfToken == null) {
  13. print("no token,request token firstly...");
  14. dio.lock();
  15. //print(dio.interceptors.requestLock.locked);
  16. return tokenDio.get("/token").then((d) {
  17. options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
  18. print("request token succeed, value: " + d.data['data']['token']);
  19. print(
  20. 'continue to perform request:path:${options.path},baseURL:${options.path}');
  21. return options;
  22. }).whenComplete(() => dio.unlock()); // unlock the dio
  23. } else {
  24. options.headers["csrfToken"] = csrfToken;
  25. return options;
  26. }
  27. }, onError: (DioError error) {
  28. //print(error);
  29. // Assume 401 stands for token expired
  30. if (error.response?.statusCode == 401) {
  31. RequestOptions options = error.response.request;
  32. // If the token has been updated, repeat directly.
  33. if (csrfToken != options.headers["csrfToken"]) {
  34. options.headers["csrfToken"] = csrfToken;
  35. //repeat
  36. return dio.request(options.path, options: options);
  37. }
  38. // update token and repeat
  39. // Lock to block the incoming request until the token updated
  40. dio.lock();
  41. dio.interceptors.responseLock.lock();
  42. dio.interceptors.errorLock.lock();
  43. return tokenDio.get("/token").then((d) {
  44. //update csrfToken
  45. options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
  46. }).whenComplete(() {
  47. dio.unlock();
  48. dio.interceptors.responseLock.unlock();
  49. dio.interceptors.errorLock.unlock();
  50. }).then((e) {
  51. //repeat
  52. return dio.request(options.path, options: options);
  53. });
  54. }
  55. return error;
  56. }));
  57. _onResult(d) {
  58. print("request ok!");
  59. }
  60. await Future.wait([
  61. dio.get("/test?tag=1").then(_onResult),
  62. dio.get("/test?tag=2").then(_onResult),
  63. dio.get("/test?tag=3").then(_onResult)
  64. ]);
  65. }

日志

我们可以添加LogInterceptor拦截器来自动打印请求、响应日志, 如:

  1. dio.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志

由于拦截器队列的执行顺序是FIFO,如果把log拦截器添加到了最前面,则后面拦截器对options的更改就不会被打印(但依然会生效), 所以建议把log拦截添加到队尾。

Cookie管理

dio_cookie_manager 包是Dio的一个插件,它提供了一个Cookie管理器

  1. import 'package:dio/dio.dart';
  2. import 'package:dio_cookie_manager/dio_cookie_manager.dart';
  3. import 'package:cookie_jar/cookie_jar.dart';
  4. main() async {
  5. var dio = Dio();
  6. var cookieJar=CookieJar();
  7. dio.interceptors.add(CookieManager(cookieJar));
  8. // Print cookies
  9. print(cookieJar.loadForRequest(Uri.parse("https://baidu.com/")));
  10. // second request with the cookie
  11. await dio.get("https://baidu.com/");
  12. ...
  13. }

自定义拦截器

开发者可以通过继承 类来实现自定义拦截器,这是一个简单的缓存示例拦截器

错误处理

当请求过程中发生错误时, Dio 会包装 Error/Exception 为一个 DioError

  1. try {
  2. //404
  3. await dio.get("https://wendux.github.io/xsddddd");
  4. } on DioError catch(e) {
  5. // The request was made and the server responded with a status code
  6. // that falls out of the range of 2xx and is also not 304.
  7. if(e.response) {
  8. print(e.response.data)
  9. print(e.response.headers)
  10. print(e.response.request)
  11. } else{
  12. // Something happened in setting up or sending the request that triggered an Error
  13. print(e.request)
  14. print(e.message)
  15. }
  16. }

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;

  /// 错误类型,见下文
  DioErrorType type;

  ///原始的error或exception对象,通常type为DEFAULT时存在。
  dynamic error;
}

DioErrorType

enum DioErrorType {
  /// When opening  url timeout, it occurs.
  CONNECT_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,
}

使用application/x-www-form-urlencoded编码

默认情况下, Dio 会将请求数据(除过String类型)序列化为 JSON. 如果想要以 application/x-www-form-urlencoded格式编码, 你可以显式设置contentType :

//Instance level
dio.options.contentType= Headers.formUrlEncodedContentType;
//or works once
dio.post("/info", data:{"id":5}, 
         options: Options(contentType:Headers.formUrlEncodedContentType ));

FormData

您也可以使用Dio发送form data,它将在multipart/form-data中发送数据,并且它支持上载文件

FormData formData = FormData.fromMap({
    "name": "wendux",
    "age": 25,
    "file": await MultipartFile.fromFile("./text.txt",filename: "upload.txt")
});
response = await dio.post("/info", data: formData);

There is a complete example here.

import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/adapter.dart';

void showProgress(received, total) {
  if (total != -1) {
    print((received / total * 100).toStringAsFixed(0) + "%");
  }
}

Future<FormData> FormData1() async {
  return FormData.fromMap({
    "name": "wendux",
    "age": 25,
    "file":
        await MultipartFile.fromFile("./example/xx.png", filename: "xx.png"),
    "files": [
      await MultipartFile.fromFile("./example/upload.txt",
          filename: "upload.txt"),
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ]
  });
}

Future<FormData> FormData2() async {
  var formData = FormData();
  formData.fields..add(MapEntry("name", "wendux"))..add(MapEntry("age", "25"));

  formData.files.add(MapEntry(
    "file",
    await MultipartFile.fromFile("./example/xx.png", filename: "xx.png"),
  ));

  formData.files.addAll([
    MapEntry(
      "files[]",
      await MultipartFile.fromFile("./example/upload.txt",
          filename: "upload.txt"),
    ),
    MapEntry(
      "files[]",
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ),
  ]);
  return formData;
}

Future<FormData> FormData3() async {
  return FormData.fromMap({
    "file": await MultipartFile.fromFile("./example/upload.txt",
        filename: "uploadfile"),
  });
}

/// FormData will create readable "multipart/form-data" streams.
/// It can be used to submit forms and file uploads to http server.
main() async {
  var dio = Dio();
  dio.options.baseUrl = "http://localhost:3000/";
  dio.interceptors.add(LogInterceptor());
  //dio.interceptors.add(LogInterceptor(requestBody: true));
  (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
      (HttpClient client) {
    client.findProxy = (uri) {
      //proxy all request to localhost:8888
      return "PROXY localhost:8888";
    };
    client.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true;
  };
  Response response;

  var formData1 = await FormData1();
  var formData2 = await FormData2();
  var bytes1 = await formData1.readAsBytes();
  var bytes2 = await formData2.readAsBytes();
  assert(bytes1.length == bytes2.length);

  var t = await FormData3();
  print(utf8.decode(await t.readAsBytes()));

  response = await dio.post(
    //"/upload",
    "http://localhost:3000/upload",
    data: await FormData3(),
    onSendProgress: (received, total) {
      if (total != -1) {
        print((received / total * 100).toStringAsFixed(0) + "%");
      }
    },
  );
  print(response);
}

多文件上传

多文件上传时,通过给key加中括号“[]”方式作为文件数组的标记,大多数后台也会通过key[]这种方式来读取。不过RFC中并没有规定多文件上传就必须得加“[]”,所以有时不带“[]”也是可以的,关键在于后台和客户端得一致
v3.0.0 以后通过Formdata.fromMap()创建的Formdata,如果有文件数组,是默认会给key加上“[]”的,比如:

  FormData.fromMap({
    "files": [
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ]
  });

最终编码时会key会为 “files[]”,如果不想添加“[]”,可以通过Formdata的API来构建:

  var formData = FormData();
  formData.files.addAll([
    MapEntry(
      "files",
       MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ),
    MapEntry(
      "files",
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ),
  ]);

转换器

转换器Transformer 用于对请求数据和响应数据进行编解码处理。Dio实现了一个默认转换器DefaultTransformer作为默认的 Transformer.
如果你想对请求/响应数据进行自定义编解码处理,可以提供自定义转换器,通过dio.transformer设置。

请求转换器 Transformer.transformRequest(…) 只会被用于 ‘PUT’、 ‘POST’、 ‘PATCH’方法,因为只有这些方法才可以携带请求体(request body)。但是响应转换器 Transformer.transformResponse() 会被用于所有请求方法的返回数据。

Flutter中设置

如果你在开发Flutter应用,强烈建议json的解码通过compute方法在后台进行,这样可以避免在解析复杂json时导致的UI卡顿。

注意,根据笔者实际测试,发现通过compute在后台解码json耗时比直接解码慢很多,建议开发者仔细评估。

// 必须是顶层函数
_parseAndDecode(String response) {
  return jsonDecode(response);
}

parseJson(String text) {
  return compute(_parseAndDecode, text);
}

void main() {
  ...
  // 自定义 jsonDecodeCallback
  (dio.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
  runApp(MyApp());
}

其它示例

这里有一个 自定义Transformer的示例.

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

/// If the request data is a `List` type, the [DefaultTransformer] will send data
/// by calling its `toString()` method. However, normally the List object is
/// not expected for request data( mostly need Map ). So we provide a custom
/// [Transformer] that will throw error when request data is a `List` type.
class MyTransformer extends DefaultTransformer {
  @override
  Future<String> transformRequest(RequestOptions options) async {
    if (options.data is List<String>) {
      throw DioError(error: "Can't send List to sever directly");
    } else {
      return super.transformRequest(options);
    }
  }

  /// The [Options] doesn't contain the cookie info. we add the cookie
  /// info to [Options.extra], and you can retrieve it in [ResponseInterceptor]
  /// and [Response] with `response.request.extra["cookies"]`.
  @override
  Future transformResponse(
      RequestOptions options, ResponseBody response) async {
    options.extra["self"] = 'XX';
    return super.transformResponse(options, response);
  }
}

main() async {
  var dio = Dio();
  // Use custom Transformer
  dio.transformer = MyTransformer();

  Response response = await dio.get("https://www.baidu.com");
  print(response.request.extra["self"]);

  try {
    await dio.post("https://www.baidu.com", data: ["1", "2"]);
  } catch (e) {
    print(e);
  }
  print("xxx");
}

执行流

虽然在拦截器中也可以对数据进行预处理,但是转换器主要职责是对请求/响应数据进行编解码,之所以将转化器单独分离,

  • 一是为了和拦截器解耦
  • 二是为了不修改原始请求数据(如果你在拦截器中修改请求数据(options.data),会覆盖原始请求数据,而在某些时候您可能需要原始请求数据)

Dio的请求流是:
请求拦截器 => 请求转换器 => 发起请求 => 响应转换器=>响应拦截器 => 最终结果。
这是一个自定义转换器的示例

HttpClientAdapter

HttpClientAdapter是 Dio 和 HttpClient之间的桥梁。2.0抽象出adapter主要是方便切换、定制底层网络库

  • Dio实现了一套标准的、强大API,而HttpClient则是真正发起Http请求的对象。
  • 我们通过HttpClientAdapter将Dio和HttpClient解耦,这样一来便可以自由定制Http请求的底层实现,比如,
    • 在Flutter中我们可以通过自定义HttpClientAdapter将Http请求转发到Native中,然后再由Native统一发起请求
    • 再比如,假如有一天OKHttp提供了dart版,你想使用OKHttp发起http请求,那么你便可以通过适配器来无缝切换到OKHttp,而不用改之前的代码。

      Dio 使用DefaultHttpClientAdapter作为其默认HttpClientAdapter,DefaultHttpClientAdapter使用dart:io:HttpClient 来发起网络请求

dio.httpClientAdapter = new DefaultHttpClientAdapter();

Here is a simple example to custom adapter.

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

class MyAdapter extends HttpClientAdapter {
  DefaultHttpClientAdapter _adapter = DefaultHttpClientAdapter();

  @override
  Future<ResponseBody> fetch(RequestOptions options,
      Stream<List<int>> requestStream, Future cancelFuture) async {
    Uri uri = options.uri;
    // hook requests to  google.com
    if (uri.host == "google.com") {
      return ResponseBody.fromString("Too young too simple!", 200);
    }
    return _adapter.fetch(options, requestStream, cancelFuture);
  }

  @override
  void close({bool force = false}) {
    _adapter.close(force: force);
  }
}

main() async {
  var dio = Dio();
  dio.httpClientAdapter = MyAdapter();
  Response response = await dio.get("https://google.com");
  print(response);
  response = await dio.get("https://baidu.com");
  print(response);
}

设置Http代理

DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 dart:io:HttpClient的代理,我们想使用代理,可以参考下面代码:

import 'package:dio/dio.dart';
import 'package:dio/adapter.dart';
...
(dio.httpClientAdapter as 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();
};

完整的示例请查看这里.

Https证书校验

有两种方法可以校验https证书,假设我们的后台服务使用的是自签名证书,证书格式是PEM格式,我们将证书的内容保存在本地字符串中,那么我们的校验逻辑如下:

String PEM="XXXXX"; // certificate content
(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;
    };
};

X509Certificate是证书的标准格式,包含了证书除私钥外所有信息,读者可以自行查阅文档。另外,上面的示例没有校验host,是因为只要服务器返回的证书内容和本地的保存一致就已经能证明是我们的服务器了(而不是中间人),host验证通常是为了防止证书和域名不匹配。
对于自签名的证书,我们也可以将其添加到本地证书信任链中,这样证书验证时就会自动通过,而不会再走到badCertificateCallback回调中:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
    SecurityContext sc = SecurityContext();
    //file is the path of certificate
    sc.setTrustedCertificates(file);
    HttpClient httpClient = HttpClient(context: sc);
    return httpClient;
};

注意,通过setTrustedCertificates()设置的证书格式必须为PEM或PKCS12,如果证书格式为PKCS12,则需将证书密码传入,这样则会在代码中暴露证书密码,所以客户端证书校验不建议使用PKCS12格式的证书。

请求取消

你可以通过cancel token来取消发起的请求:

CancelToken token = CancelToken();
dio.get(url, cancelToken: token)
    .catchError((DioError err){
        if (CancelToken.isCancel(err)) {
            print('Request canceled! '+ err.message)
        }else{
            // handle error.
        }
    });
// cancel the requests with "cancelled" message.
token.cancel("cancelled");

注意: 同一个cancel token 可以用于多个请求,当一个cancel token取消时,所有使用该cancel token的请求都会被取消。

完整的示例请参考取消示例.

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

main() async {
  var dio = Dio();
  dio.interceptors.add(LogInterceptor());
  // Token can be shared with different requests.
  CancelToken token = CancelToken();
  // In one minute, we cancel!
  Timer(Duration(milliseconds: 500), () {
    token.cancel("cancelled");
  });

  // The follow three requests with the same token.
  var url1 = "https://www.google.com";
  var url2 = "https://www.facebook.com";
  var url3 = "https://www.baidu.com";

  await Future.wait([
    dio
        .get(url1, cancelToken: token)
        .then((response) => print('${response.request.path}: succeed!'))
        .catchError(
      (e) {
        if (CancelToken.isCancel(e)) {
          print('$url1: $e');
        }
      },
    ),
    dio
        .get(url2, cancelToken: token)
        .then((response) => print('${response.request.path}: succeed!'))
        .catchError((e) {
      if (CancelToken.isCancel(e)) {
        print('$url2: $e');
      }
    }),
    dio
        .get(url3, cancelToken: token)
        .then((response) => print('${response.request.path}: succeed!'))
        .catchError((e) {
      if (CancelToken.isCancel(e)) {
        print('$url3: $e');
      }
      print(e);
    })
  ]);
}

继承 Dio class

Dio 是一个拥有factory 构造函数的接口类,因此不能直接继承 Dio ,但是可以通过 DioForNative 或DioForBrowser 来间接实现:

import 'package:dio/dio.dart';
import 'package:dio/native_imp.dart'; //在浏览器中, import 'package:dio/browser_imp.dart'
class Http extends DioForNative {
  Http([BaseOptions options]):super(options){
    // 构造函数做一些事
  }
}

我们也可以直接实现 Dio接口类 :

class MyDio with DioMixin implements Dio{
  // ...
}

示例目录

你可以在这里查看dio的全部示例.

实例

我们通过Github开放的API来请求flutterchina组织下的所有公开的开源项目,实现:

  1. 在请求阶段弹出loading
  2. 请求结束后,如果请求失败,则展示错误信息;如果成功,则将项目名称列表展示出来。

代码如下:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: true,

        title: 'FutureBuilderRoute网络请求',

        theme: ThemeData(
          primarySwatch: Colors.blue,

        ),
        home: Scaffold(
          appBar: AppBar(title: Text("FutureBuilderRoute网络请求")),
          body: FutureBuilderRoute(),
        ));
  }
}

class FutureBuilderRoute extends StatefulWidget {
  FutureBuilderRoute({Key key}) : super(key: key);

  @override
  _FutureBuilderRouteState createState() => _FutureBuilderRouteState();
}


class _FutureBuilderRouteState extends State<FutureBuilderRoute> {
  Dio _dio = new Dio();
  @override
  Widget build(BuildContext context) {
    return new Container(
      alignment: Alignment.center,
      child: FutureBuilder(
          future: _dio.get("https://api.github.com/orgs/flutterchina/repos"),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            //请求完成
            if (snapshot.connectionState == ConnectionState.done) {
              Response response = snapshot.data;
              //发生错误
              if (snapshot.hasError) {
                return Text(snapshot.error.toString());
              }
              //请求成功,通过项目信息构建用于显示项目名称的ListView
              return ListView(
                children: response.data.map<Widget>((e) =>
                    ListTile(title: Text(e["full_name"]))
                ).toList(),
              );
            }
            //请求未完成时弹出loading
            return CircularProgressIndicator();
          }
      ),
    );
  }
}

Http2支持

dio_http2_adapter包提供了一个支持Http/2.0的Adapter,详情可以移步 dio_http2_adapter