flutter新版dio封装

来源:juejin.cn 更新时间:2023-05-25 21:55

转载:作者github: yuexunshi (github.com)

分析需求

为什么要封装?

  • 全局token验证

  • 自定义拦截器

  • 缓存处理

  • 统一封装业务错误逻辑

  • 代理配置

  • 重试机制

  • log输出

  • 自定义解析,数据脱壳

要初始化哪些配置?

  • 域名
  • 代理地址
  • cookie本地缓存地址
  • 超时时间
  • 自定义拦截器

定义一个配置信息类去初始化这些配置:

// dio 配置项
class HttpConfig {
  final String? baseUrl;
  final String? proxy;
  final String? cookiesPath;
  final List<Interceptor>? interceptors;
  final int connectTimeout;
  final int sendTimeout;
  final int receiveTimeout;

  HttpConfig({
    this.baseUrl,
    this.proxy,
    this.cookiesPath,
    this.interceptors,
    this.connectTimeout = Duration.millisecondsPerMinute,
    this.sendTimeout = Duration.millisecondsPerMinute,
    this.receiveTimeout = Duration.millisecondsPerMinute,
  });

  // static DioConfig of() => Get.find<DioConfig>();
}

请求差异化有哪些配置?

  • 解析策略

    许多公司接口规范经历过变更,有多个返回类型,那么就需要针对不同的数据类型,做不同的解析。

    比如旧版本:

    // 旧版本
    {
        "code": 1,
        "data": {},
        "state": true
    }
    // 新版本
    {
        "code": 1,
        "data": {
          "data": {},
          "hasmore":false
        },
        "message": “success”
    }
    

    要做到脱壳,拿到解析后的data,就需要两种解析策略。所以需要根据不同接口动态配置解析策略。

  • path

  • 参数

  • cancelToken

  • dio 的常用参数

    Dio 的请求参数已经很全面的包括了分析出的配置参数,只需要另添加一个解析策略即可。

    遵守 SOLID 原则定义一个抽象解析策略:

    /// Response 解析
    abstract class HttpTransformer {
      HttpResponse parse(Response response);
    }
    
    

    根据实际需求默认实现:

    class DefaultHttpTransformer extends HttpTransformer {
    // 假设接口返回类型
    //   {
    //     "code": 100,
    //     "data": {},
    //     "message": "success"
    // }
      @override
      HttpResponse parse(Response response) {
        // if (response.data["code"] == 100) {
        //   return HttpResponse.success(response.data["data"]);
        // } else {
        // return HttpResponse.failure(errorMsg:response.data["message"],errorCode: response.data["code"]);
        // }
        return HttpResponse.success(response.data["data"]);
      }
    
      /// 单例对象
      static DefaultHttpTransformer _instance = DefaultHttpTransformer._internal();
    
      /// 内部构造方法,可避免外部暴露构造函数,进行实例化
      DefaultHttpTransformer._internal();
    
      /// 工厂构造方法,这里使用命名构造函数方式进行声明
      factory DefaultHttpTransformer.getInstance() => _instance;
    }
    

    单例模式是为了避免多次创建实例。方便下一步使用。

异常处理

异常大体分为以下几种:

  • 网络异常
  • 客户端请求异常
  • 服务端异常

客户端异常又可拆分两种常见的异常:请求参数或路径错误,鉴权失败/token失效

异常归档后创建异常:

class HttpException implements Exception {
  final String? _message;

  String get message => _message ?? this.runtimeType.toString();

  final int? _code;

  int get code => _code ?? -1;

  HttpException([this._message, this._code]);

  String toString() {
    return "code:$code--message=$message";
  }
}

/// 客户端请求错误
class BadRequestException extends HttpException {
  BadRequestException({String? message, int? code}) : super(message, code);
}
/// 服务端响应错误
class BadServiceException extends HttpException {
  BadServiceException({String? message, int? code}) : super(message, code);
}



class UnknownException extends HttpException {
  UnknownException([String? message]) : super(message);
}

class CancelException extends HttpException {
  CancelException([String? message]) : super(message);
}

class NetworkException extends HttpException {
  NetworkException({String? message, int? code}) : super(message, code);
}

/// 401
class UnauthorisedException extends HttpException {
  UnauthorisedException({String? message, int? code = 401}) : super(message);
}

class BadResponseException extends HttpException {
  dynamic? data;

  BadResponseException([this.data]) : super();
}

返回数据类型

返回的数据类型,需要有成功或是失败的标识,还需要脱壳后的数据,如果失败了,也需要失败的信息,定义几个工厂方法方便创建实例:

class HttpResponse {
  late bool ok;
  dynamic? data;
  HttpException? error;

  HttpResponse._internal({this.ok = false});

  HttpResponse.success(this.data) {
    this.ok = true;
  }

  HttpResponse.failure({String? errorMsg, int? errorCode}) {
    this.error = BadRequestException(message: errorMsg, code: errorCode);
    this.ok = false;
  }

  HttpResponse.failureFormResponse({dynamic? data}) {
    this.error = BadResponseException(data);
    this.ok = false;
  }

  HttpResponse.failureFromError([HttpException? error]) {
    this.error = error ?? UnknownException();
    this.ok = false;
  }
}

开始封装

配置 Dio

Dio 配置组装,需要我们定义一个初始化类,用于把请求的初始化配置添加进去。一般可以定义一个单例类,init方法里去初始化一个 Dio ,也可以采用实现 Dio 的方式:

class AppDio with DioMixin implements Dio {
  AppDio({Baseoptions? options, HttpConfig? dioConfig}) {
    options ??= BaseOptions(
      baseUrl: dioConfig?.baseUrl ?? "",
      contentType: 'application/json',
      connectTimeout: dioConfig?.connectTimeout,
      sendTimeout: dioConfig?.sendTimeout,
      receiveTimeout: dioConfig?.receiveTimeout,
    );
    this.options = options;

    // DioCacheManager
    final cacheOptions = CacheOptions(
      // A default store is required for interceptor.
      store: MemCacheStore(),
      // Optional. Returns a cached response on error but for statuses 401 & 403.
      hitCacheOnErrorExcept: [401, 403],
      // Optional. Overrides any HTTP directive to delete entry past this duration.
      maxStale: const Duration(days: 7),
    );
    interceptors.add(DioCacheInterceptor(options: cacheOptions));
    // Cookie管理
    if (dioConfig?.cookiesPath?.isNotEmpty ?? false) {
      interceptors.add(CookieManager(
          PersistCookieJar(storage: FileStorage(dioConfig!.cookiesPath))));
    }

    if (kDebugMode) {
      interceptors.add(LogInterceptor(
          responseBody: true,
          error: true,
          requestHeader: false,
          responseHeader: false,
          request: false,
          requestBody: true));
    }
    if (dioConfig?.interceptors?.isNotEmpty ?? false) {
      interceptors.addAll(interceptors);
    }
    httpClientAdapter = DefaultHttpClientAdapter();
    if (dioConfig?.proxy?.isNotEmpty ?? false) {
      setProxy(dioConfig!.proxy!);
    }
  }

  setProxy(String proxy) {
    (httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
        (client) {
      // config the http client
      client.findProxy = (uri) {
        // proxy all request to localhost:8888
        return "PROXY $proxy";
      };
      // you can also create a HttpClient to dio
      // return HttpClient();
    };
  }
}


Restful请求

采用 Restful 标准,创建对应的请求方法:

class HttpClient {
  late AppDio _dio;

  HttpClient({BaseOptions? options, HttpConfig? dioConfig})
      : _dio = AppDio(options: options, dioConfig: dioConfig);

  Future<HttpResponse> get(String uri,
      {Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      ProgressCallback? onReceiveProgress,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.get(
        uri,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onReceiveProgress: onReceiveProgress,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> post(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      ProgressCallback? onSendProgress,
      ProgressCallback? onReceiveProgress,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.post(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> patch(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      ProgressCallback? onSendProgress,
      ProgressCallback? onReceiveProgress,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.patch(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> delete(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.delete(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> put(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.put(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<Response> download(String urlPath, savePath,
      {ProgressCallback? onReceiveProgress,
      Map<String, dynamic>? queryParameters,
      CancelToken? cancelToken,
      bool deleteOnError = true,
      String lengthHeader = Headers.contentLengthHeader,
      data,
      Options? options,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.download(
        urlPath,
        savePath,
        onReceiveProgress: onReceiveProgress,
        queryParameters: queryParameters,
        cancelToken: cancelToken,
        deleteOnError: deleteOnError,
        lengthHeader: lengthHeader,
        data: data,
        options: data,
      );
      return response;
    } catch (e) {
      throw e;
    }
  }
}

响应解析

得到请求数据后,解析为定义的通用返回数据类型,需要首先判断是否取得返回值,然后判断网络请求成功,网络请求成功之后,采取判断是否接口返回期望的数据,还是因为请求参数错误或者服务器错误返回了错误信息。如果错误了,把错误信息格式化为定义的异常:

HttpResponse handleResponse(Response? response,
    {HttpTransformer? httpTransformer}) {
  httpTransformer ??= DefaultHttpTransformer.getInstance();

  // 返回值异常
  if (response == null) {
    return HttpResponse.failureFromError();
  }

  // token失效
  if (_isTokenTimeout(response.statusCode)) {
    return HttpResponse.failureFromError(
        UnauthorisedException(message: "没有权限", code: response.statusCode));
  }
  // 接口调用成功
  if (_isRequestSuccess(response.statusCode)) {
    return httpTransformer.parse(response);
  } else {
    // 接口调用失败
    return HttpResponse.failure(
        errorMsg: response.statusMessage, errorCode: response.statusCode);
  }
}

HttpResponse handleException(Exception exception) {
  var parseException = _parseException(exception);
  return HttpResponse.failureFromError(parseException);
}

/// 鉴权失败
bool _isTokenTimeout(int? code) {
  return code == 401;
}

/// 请求成功
bool _isRequestSuccess(int? statusCode) {
  return (statusCode != null && statusCode >= 200 && statusCode < 300);
}

HttpException _parseException(Exception error) {
  if (error is DioError) {
    switch (error.type) {
      case DioErrorType.connectTimeout:
      case DioErrorType.receiveTimeout:
      case DioErrorType.sendTimeout:
        return NetworkException(message: error.error.message);
      case DioErrorType.cancel:
        return CancelException(error.error.message);
      case DioErrorType.response:
        try {
          int? errCode = error.response?.statusCode;
          switch (errCode) {
            case 400:
              return BadRequestException(message: "请求语法错误", code: errCode);
            case 401:
              return UnauthorisedException(message: "没有权限", code: errCode);
            case 403:
              return BadRequestException(message: "服务器拒绝执行", code: errCode);
            case 404:
              return BadRequestException(message: "无法连接服务器", code: errCode);
            case 405:
              return BadRequestException(message: "请求方法被禁止", code: errCode);
            case 500:
              return BadServiceException(message: "服务器内部错误", code: errCode);
            case 502:
              return BadServiceException(message: "无效的请求", code: errCode);
            case 503:
              return BadServiceException(message: "服务器挂了", code: errCode);
            case 505:
              return UnauthorisedException(
                  message: "不支持HTTP协议请求", code: errCode);
            default:
              return UnknownException(error.error.message);
          }
        } on Exception catch (_) {
          return UnknownException(error.error.message);
        }

      case DioErrorType.other:
        if (error.error is SocketException) {
          return NetworkException(message: error.message);
        } else {
          return UnknownException(error.message);
}
      default:
        return UnknownException(error.message);
    }
  } else {
    return UnknownException(error.toString());
  }
}


缓存、重试、401拦截

默认的通用拦截器在 AppDio里直接定义,如果需要额外配置的拦截器,从HttpConfig里传入。

这些拦截器的创建,可以参考上一篇强大的dio封装,可能满足你的一切需要,这里就不再赘述。

使用

第一步,全局配置并初始化:

  HttpConfig dioConfig =
      HttpConfig(baseUrl: "https://gank.io/", proxy: "192.168.2.249:8888");
  HttpClient client = HttpClient(dioConfig: dioConfig);
  Get.put<HttpClient>(client);

请求:

  void get() async {
    HttpResponse appResponse = await dio.get("api/v2/banners");
    if (appResponse.ok) {
      debugPrint("====" + appResponse.data.toString());
    } else {
      debugPrint("====" + appResponse.error.toString());
    }
  }


附上开发环境:

[?] Flutter (Channel stable, 2.0.5, on Mac OS X 10.15.7 19H15 darwin-x64, locale zh-Hans-CN)