Flutter入门-网络请求Dio

393 阅读13分钟

一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等...

dependencies: dio: ^x.x.x // latest version

使用

基本使用

// 完整写法
response=await request("/test", data: {"id":12,"name":"xx"}, new Options(method:"GET"));

//发起一个 `GET` 请求 :
Response response;
response=await dio.get("/test?id=12&name=wendu")
print(response.data.toString());
// 请求参数也可以通过对象传递,上面的代码等同于:
response=await dio.get("/test",data:{"id":12,"name":"wendu"})
print(response.data.toString());

//发起一个 `POST` 请求:
response=await dio.post("/test",data:{"id":12,"name":"wendu"})

//发起多个并发请求:
response= await Future.wait([dio.post("/info"),dio.get("/token")]);

//下载文件:
response=await dio.download("https://www.google.com/","./xx.html")

//发送 FormData:
FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
});
response = await dio.post("/info", data: formData)

//通过FormData上传多个文件:
FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
   "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt")
   "file2": new UploadFileInfo(new File("./upload.txt"), "upload2.txt")
});
response = await dio.post("/info", data: formData)

…你可以在这里获取所有示例代码.

配置

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

Dio dio = new Dio; // 使用默认配置

// 配置dio实例
dio.options.baseUrl="https://www.xx.com/api" 
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout=3000;  

// 或者通过传递一个 `options`来创建dio实例
Options options= new Options(
   baseUrl:"https://www.xx.com/api",
   connectTimeout:5000,
   receiveTimeout:3000
);
Dio dio = new Dio(options);

配置项

{
  /// Http method.
  String method;

  /// 请求基地址,可以包含子路径,如: "https://www.google.com/api/".
  String baseUrl;

  /// Http请求头.
  Map<String, dynamic> headers;

  /// 连接服务器超时时间,单位是毫秒.
  int connectTimeout;

  ///  响应流上前后两次接受到数据的间隔,单位为毫秒。如果两次间隔超过[receiveTimeout],
  ///  [Dio] 将会抛出一个[DioErrorType.RECEIVE_TIMEOUT]的异常.
  ///  注意: 这并不是接收数据的总时限.
  int receiveTimeout;

  /// 请求数据,可以是任意类型.
  var data;

  /// 请求路径,如果 `path` 以 "http(s)"开始, 则 `baseURL` 会被忽略; 否则,
  /// 将会和baseUrl拼接出完整的的url.
  String path="";

  /// 请求的Content-Type,默认值是[ContentType.JSON].
  /// 如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
  /// 可以设置此选项为 `ContentType.parse("application/x-www-form-urlencoded")`,  这样[Dio]
  /// 就会自动编码请求体.
  ContentType contentType;

  /// [responseType] 表示期望以那种格式(方式)接受响应数据。
  /// 目前 [ResponseType] 接受三种类型 `JSON`, `STREAM`, `PLAIN`.
  ///
  /// 默认值是 `JSON`, 当响应头中content-type为"application/json"时,dio 会自动将响应内容转化为json对象。
  /// 如果想以二进制方式接受响应数据,如下载一个二进制文件,那么可以使用 `STREAM`.
  ///
  /// 如果想以文本(字符串)格式接收响应数据,请使用 `PLAIN`.
  ResponseType responseType;

  /// 用户自定义字段,可以在 [Interceptor]、[TransFormer] 和 [Response] 中取到.
  Map<String, dynamic> extra;
}

响应

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

{
  /// 响应数据,可能已经被转换了类型, 详情请参考Options中的[ResponseType].
  var data;
  /// 响应头
  HttpHeaders headers;
  /// 本次请求信息
  Options request;
  /// Http status code.
  int statusCode;
  /// 响应对象的自定义字段(可以在拦截器中设置它),调用方可以在`then`中获取.
  Map<String, dynamic> extra;
}

拦截器

每一个 Dio 实例都有一个请求拦截器 RequestInterceptor 和一个响应拦截器 ResponseInterceptor, 通过拦截器你可以在请求之前或响应之后(但还没有被 then 或 catchError处理)做一些统一的预处理操作

 dio.interceptor.request.onSend = (Options options){
     // 在请求被发送之前做一些事情
     return options; //continue
     // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。
     // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data.
     //
     // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`,
     // 这样请求将被中止并触发异常,上层catchError会被调用。   
 }
 dio.interceptor.response.onSuccess = (Response response) {
     // 在返回响应数据之前做一些预处理
     return response; // continue
 };
 dio.interceptor.response.onError = (DioError e){
     // 当请求失败时做一些预处理
     return DioError;//continue
 }    

如果你想移除拦截器,你可以将它们置为null:

 代码解读
复制代码
dio.interceptor.request.onSend=null;
dio.interceptor.response.onSuccess=null;
dio.interceptor.response.onError=null;

完成和终止请求/响应

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

 dio.interceptor.request.onSend = (Options options){
     return dio.resolve("fake data")    
 }
 Response response= await dio.get("/test");
 print(response.data);//"fake data"

拦截器中支持异步任务

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

 代码解读
复制代码
  dio.interceptor.request.onSend = (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   
 }

Lock/unlock 拦截器

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

 代码解读
复制代码
tokenDio=new Dio(); //Create a new instance to request the token.
tokenDio.options=dio;
dio.interceptor.request.onSend = (Options options) async{
     // If no token, request token firstly and lock this interceptor
     // to prevent other request enter this interceptor.
     dio.interceptor.request.lock(); 
     // We use a new 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.interceptor.request.unlock() 
     return options; //continue   
 }

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

dio.lock() == dio.interceptor.request.lock()

dio.unlock() == dio.interceptor.request.unlock()

示例

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

dio.interceptor.request.onSend = (Options options) {
    print('send request:path:${options.path},baseURL:${options.baseUrl}');
    if (csrfToken == null) {
      print("no token,request token firstly...");
      //lock the dio.
      dio.lock();
      return tokenDio.get("/token").then((d) {
        options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
        print("request token succeed, value: " + d.data['data']['token']);
        print('continue to perform request:path:${options.path},baseURL:${options.path}');
        return options;
      }).whenComplete(() => dio.unlock()); // unlock the dio
    } else {
      options.headers["csrfToken"] = csrfToken;
      return options;
    }
  };

完整的示例代码请点击 这里.

错误处理

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

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

DioError 字段

 {
  /// 响应信息, 如果错误发生在在服务器返回数据之前,它为 `null`
  Response response;

  /// 错误描述.
  String message;
  
  /// 错误类型,见下文
  DioErrorType type;

  /// 错误栈信息,可能为null
  StackTrace stackTrace;
}

DioErrorType

enum DioErrorType {
  /// Default error type, usually occurs before connecting the server.
  DEFAULT,

  /// When opening  url timeout, it occurs.
  CONNECT_TIMEOUT,

  ///  Whenever more than [receiveTimeout] (in milliseconds) passes between two events from response stream,
  ///  [Dio] will throw the [DioError] with [DioErrorType.RECEIVE_TIMEOUT].
  ///
  ///  Note: This is not the receiving time limitation.
  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
}

编码方式

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

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

//Instance level
dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");
//or works once
dio.post("/info",data:{"id":5}, options: new Options(contentType:ContentType.parse("application/x-www-form-urlencoded")))    

这里有一个示例.

FormData

Dio支持发送 FormData, 请求数据将会以 multipart/form-data方式编码, FormData中可以一个或多个包含文件 .

 代码解读
复制代码
FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
    "file": new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
});
response = await dio.post("/info", data: formData)

注意: 只有 post 方法支持发送 FormData.

这里有一个完整的示例.

转换器

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

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

执行流

虽然在拦截器中也可以对数据进行预处理,但是转换器主要职责是对请求/响应数据进行编解码,之所以将转化器单独分离,一是为了和拦截器解耦,二是为了不修改原始请求数据(如果你在拦截器中修改请求数据(options.data),会覆盖原始请求数据,而在某些时候您可能需要原始请求数据). Dio的请求流是:

请求拦截器 >> 请求转换器 >> 发起请求 >> 响应转换器 >> 响应拦截器 >> 最终结果

这是一个自定义转换器的示例.

设置Http代理

Dio 是使用 HttpClient发起的http请求,所以你可以通过配置 httpClient来支持代理,示例如下:

 代码解读
复制代码
  dio.onHttpClientCreate = (HttpClient client) {
    client.findProxy = (uri) {
      //proxy all request to localhost:8888
      return "PROXY localhost:8888";
    };
  };

完整的示例请查看这里.

请求取消

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

 代码解读
复制代码
CancelToken token = new 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的请求都会被取消。

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

Cookie管理

你可以通过 cookieJar 来自动管理请求/响应cookie.

dio cookie 管理 API 是基于开源库 cookie_jar.

你可以创建一个CookieJarPersistCookieJar 来帮您自动管理cookie, dio 默认使用 CookieJar , 它会将cookie保存在内存中。 如果您想对cookie进行持久化, 请使用 PersistCookieJar , 示例代码如下:

var dio = new Dio();
dio.cookieJar=new PersistCookieJar("./cookies");

PersistCookieJar 实现了RFC中标准的cookie策略. PersistCookieJar 会将cookie保存在文件中,所以 cookies 会一直存在除非显式调用 delete 删除.

请求封装

基本配置与客户端

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

配置了请求的 baseUrl 、连接超时时间、请求成功的业务编码。如果还有需其他配置也可以统一配置到该类下。

创建 RequestClient 用于封装 dio 的请求,在类的构造方法中初始化 dio 配置:

RequestClient requestClient = RequestClient();

class RequestClient {
  late Dio _dio;

  RequestClient() {
    _dio = Dio(
        BaseOptions(baseUrl: RequestConfig.baseUrl, connectTimeout: RequestConfig.connectTimeout)
    );
  }
}

数据处理

请求数据

  _convertRequestData(data) {
    if (data != null) {
      data = jsonDecode(jsonEncode(data));
    }
    return data;
  }

  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);
  }
}

响应数据

  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);
  }

  ///请求响应内容处理
  T? _handleResponse<T>(Response response) {
    if (response.statusCode == 200) {
      ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data);
      return _handleBusinessResponse<T>(apiResponse);
    } else {
      return null;
    }
  }

  ///业务内容处理
  T? _handleBusinessResponse<T>(ApiResponse<T> response) {
    if (response.code == RequestConfig.successCode) {
      return response.data;
    } else {
      return null;
    }
  }

异常处理

异常一般分为两部分:Http异常、业务异常。

  • Http 异常:Http 错误,如 404、503 等
  • 业务异常:请求成功,但是业务异常,如:登录时用户名密码错误等

首先创建一个 ApiException 用于统一封装请求的异常信息:

class ApiException implements Exception {
  static const unknownException = "未知错误";
  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, "请求取消");
      case DioErrorType.connectTimeout:
        return BadRequestException(-1, "连接超时");
      case DioErrorType.sendTimeout:
        return BadRequestException(-1, "请求超时");
      case DioErrorType.receiveTimeout:
        return BadRequestException(-1, "响应超时");
      case DioErrorType.response:
        try {
          
          /// http 错误码带业务错误信息
          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, "请求语法错误");
            case 401:
              return UnauthorisedException(errCode!, "没有权限");
            case 403:
              return UnauthorisedException(errCode!, "服务器拒绝执行");
            case 404:
              return UnauthorisedException(errCode!, "无法连接服务器");
            case 405:
              return UnauthorisedException(errCode!, "请求方法被禁止");
            case 500:
              return UnauthorisedException(errCode!, "服务器内部错误");
            case 502:
              return UnauthorisedException(errCode!, "无效的请求");
            case 503:
              return UnauthorisedException(errCode!, "服务器异常");
            case 505:
              return UnauthorisedException(errCode!, "不支持HTTP协议请求");
            default:
              return ApiException(
                  errCode, error.response?.statusMessage ?? '未知错误');
          }
        } 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();
      return apiException;
    }
  }
}

/// 请求错误
class BadRequestException extends ApiException {
  BadRequestException([int? code, String? message]) : super(code, message);
}

/// 未认证异常
class UnauthorisedException extends ApiException {
  UnauthorisedException([int code = -1, String message = ''])
      : super(code, message);
}

ApiException 类创建好后,需要在 request 方法中捕获异常,对 request 方法改造如下:

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){
        throw exception;
      }
    }

    return null;
  }

  ///请求响应内容处理
  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);
      throw exception;
    }
  }

  ///业务内容处理
  T? _handleBusinessResponse<T>(ApiResponse<T> response) {
    if (response.code == RequestConfig.successCode) {
      return response.data;
    } else {
      var exception = ApiException(response.code, response.message);
      throw exception;
    }
  }

在 request 方法上添加了 bool Function(ApiException)? onError 参数,用于错误信息处理的回调,且返回值为 bool

request 方法中添加 try-catch 包裹,并在 catch 中创建 ApiException ,调用 onError,当 onError 返回为 true 时即错误信息已被调用方处理,则不抛出异常,否则抛出异常

多个请求嵌套问题

开发中经常会在接口请求成功后做其他处理,比如数据处理或者界面刷新等,请求失败后弹出提示或者错误处理等等,如果按照上述的封装则需要判断返回数据是否为 null 不为空进行后续处理,如果一个业务存在多个请求依赖调用,则此处则会嵌套多次,代码阅读性不好

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

为了解决上述问题,并且实现统一异常处理,创建一个顶级的 request 方法:

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;
}

request 方法有个 block 函数参数,在 request 中进行调用,并对其包裹 try-catch ,在 catch 中进行统一异常处理,当外部未处理异常时则在 handleException 中进行统一处理,如 401 则跳转登录页,其他错误统一弹出错误提示。

此时使用如下:

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

    user = await apiService.test();
    print(user?.name);
  });

当 request 包裹的代码中其中一个请求错误则不会继续向下执行。

请求拦截

dio 支持添加拦截器自定义处理请求和返回数据,只需实现自定义拦截类继承 Interceptor 实现 onRequestonResponse 即可。

比如当登录后需要给所有请求添加统一的 Header 携带 token 信息时就可以通过拦截器实现。

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);
  }
}

然后在初始化 dio 时添加拦截器即可:

_dio.interceptors.add(TokenInterceptor());

loading

前面解决异常处理使用了一个全局的 request 方法,loading 可以使用同样的思路实现,创建 loading 方法:

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: "加载中...");
}

void dismissLoading(){
 EasyLoading.dismiss();
}

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;
}