flutter新版dio封装
转载:作者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)