应用开发中,网络请求几乎是必不可少的功能,本文将介绍如何通过对 dio
进行二次封装一步一步实现网络请求封装,以便于在项目中方便快捷的使用网络请求。
封装后的网络请求将具备如下功能:
- 简单易用
- 数据解析
- 异常处理
- 请求拦截
- 日志打印
- loading 显示
下面将一步一步带你实现网络请求的封装。
添加依赖
首先在项目里添加 dio
的依赖:
1 2
| dependencies: dio: ^4.0.4
|
请求封装
首先创建一个 RequestConfig
类,用于放置 dio
的配置参数,如下:
1 2 3 4 5
| class RequestConfig{ static const baseUrl = "https://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test"; static const connectTimeout = 15000; static const successCode = 200; }
|
配置了请求的 baseUrl
、连接超时时间、请求成功的业务编码。如果还有需其他配置也可以统一配置到该类下。
创建 RequestClient
用于封装 dio
的请求,在类的构造方法中初始化 dio 配置:
1 2 3 4 5 6 7 8 9 10 11
| RequestClient requestClient = RequestClient();
class RequestClient { late Dio _dio;
RequestClient() { _dio = Dio( BaseOptions(baseUrl: RequestConfig.baseUrl, connectTimeout: RequestConfig.connectTimeout) ); } }
|
在类的上方,创建了一个全局的变量 requestClient
方便外部调用。
dio
本身提供了get
、post
、put
、delete
等一系列 http 请求方法,但是通过源码发现最终这些方法都是调用的 request
的方法实现的。所以这里直接对 dio 的 request
方法进行封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Future<dynamic> request( 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 response.data; }
|
将常用参数进行统一封装为 request 方法然后调用 dio 的 request
方法,然后再在 request 方法里进行统一的数据处理,如数据解析等。
数据解析
返回数据解析
在移动开发中,开发者习惯将返回数据解析成实体类使用,在 Flutter应用框架搭建(三)Json数据解析 一文中讲解了在 Flutter 中如何将 json 数据解析为实体类,接下来将介绍如何结合 dio 完成数据解析的封装。
项目开发中接口返回的数据结构一般是这样的:
1 2 3 4 5 6 7 8 9
| { "code": 200, "message": "success", "data":{ "id": "12312312", "name": "loongwind", "age": 18 } }
|
结合 Flutter应用框架搭建(三)Json数据解析 一文,创建 ApiResponse
类用于解析接口返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class ApiResponse<T> {
int? code; String? message; T? data;
ApiResponse();
factory ApiResponse.fromJson(Map<String, dynamic> json) => $ApiResponseFromJson<T>(json);
Map<String, dynamic> toJson() => $ApiResponseToJson(this);
@override String toString() { return jsonEncode(this); } }
|
关于 json 解析请详阅 Flutter应用框架搭建(三)Json数据解析
因为返回的数据中 data 的数据类型是不定的,所以改造 request
支持泛型,然后在 request 方法中统一进行数据解析,然后返回 data 数据,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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; } }
|
通过 ApiResponse
解析返回数据,然后判断 ApiResponse
的业务 code 是否为成功,成功则返回 data 数据。
有时候在应用里还需要调用第三方接口,但是第三方接口返回的数据结构可能会有差异,此时就需要返回原始数据单独做处理。创建一个 RawData
类,用于解析原始数据:
1 2 3
| class RawData{ dynamic value; }
|
然后修改 RequestClient
中的 _handleResponse
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ///请求响应内容处理 T? _handleResponse<T>(Response response) { if (response.statusCode == 200) { if(T.toString() == (RawData).toString()){ RawData raw = RawData(); raw.value = response.data; return raw as T; }else { ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data); return _handleBusinessResponse<T>(apiResponse); } } else { var exception = ApiException(response.statusCode, ApiException.unknownException); throw exception; } }
|
新增判断泛型是否为 RawData
,是则直接去除 response.data
放入 RawData
中返回,即 RawData
的 value 就是接口返回的原始数据。
请求数据转换
除了返回数据的解析,实际开发过程中还会遇到对请求参数的处理,比如请求参数为 json 数据,但是代码里为了方便处理使用的实体类,request 中 data 参数可能传入的是一个实体类实例,此时就需要将 data 转换为 json 数据再进行数据请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| _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); } }
|
此处使用 _convertRequestData
方法,将请求 data 数据先使用 jsonEncode
转换为字符串,再使用 jsonDecode
方法将字符串转换为 Map。
异常处理
接下来看看如何进行统一的异常处理,异常一般分为两部分:Http异常、业务异常。
- Http 异常:Http 错误,如 404、503 等
- 业务异常:请求成功,但是业务异常,如:登录时用户名密码错误等
首先创建一个 ApiException
用于统一封装请求的异常信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| 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 { 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 主要根据 DioError 信息创建 ApiException,但是仔细发现其中有一段解析返回数据让创建 ApiException 的代码,如下:
1 2 3 4
| ApiResponse apiResponse = ApiResponse.fromJson(error.response?.data); if(apiResponse.code != null){ return ApiException(apiResponse.code, apiResponse.message); }
|
是因为有些时候后端业务异常时修改了返回的 http 状态码,当 http 状态码非 200 开头时 dio 会抛出 DioError
错误,但此时需要的错误信息为 response 中的错误信息,所以这里需要先解析 response 数据获取错误信息。
ApiException 类创建好后,需要在 request 方法中捕获异常,对 request 方法改造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| 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 时即错误信息已被调用方处理,则不抛出异常,否则抛出异常。
同时为 response 数据解析的方法也加上了抛出异常的处理。当业务异常时抛出对应的业务异常信息。
经过上述封装后,确实能对异常信息进行处理,但在实际开发中有个问题,开发中经常会在接口请求成功后做其他处理,比如数据处理或者界面刷新等,请求失败后弹出提示或者错误处理等等,如果按照上述的封装则需要判断返回数据是否为 null 不为空进行后续处理,如果一个业务存在多个请求依赖调用,则此处则会嵌套多次,代码阅读性不好。如下:
1 2 3 4 5 6 7 8
| var data1 = requestClient.request(url1); if( data1 != null ){ var data2 = requestClient.request(url2); if(data2 != null){ var data3 = requestClient.request(url3); } }
|
为了解决上述问题,并且实现统一异常处理,创建一个顶级的 request 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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 ){ return true; } showError(exception.message ?? ApiException.unknownException);
return false; }
|
request 方法有个 block
函数参数,在 request 中进行调用,并对其包裹 try-catch ,在 catch 中进行统一异常处理,当外部未处理异常时则在 handleException
中进行统一处理,如 401 则跳转登录页,其他错误统一弹出错误提示。
此时使用如下:
1 2 3 4 5 6 7
| void testRequest() => request(() async { UserEntity? user = await apiService.test(); print(user?.name);
user = await apiService.test(); print(user?.name); });
|
当 request 包裹的代码中其中一个请求错误则不会继续向下执行。
请求拦截
dio 支持添加拦截器自定义处理请求和返回数据,只需实现自定义拦截类继承 Interceptor
实现 onRequest
和 onResponse
即可。
比如当登录后需要给所有请求添加统一的 Header 携带 token 信息时就可以通过拦截器实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class TokenInterceptor extends Interceptor{
@override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 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 时添加拦截器即可:
1
| _dio.interceptors.add(TokenInterceptor());
|
日志打印
开发过程中为了方便调试经常需要打印请求返回日志,可以使用自定义拦截器实现,也可以使用第三方实现的日志打印的拦截器 pretty_dio_logger
库。
添加依赖:
1
| pretty_dio_logger: ^1.1.1
|
dio 添加日期拦截器:
1
| _dio.interceptors.add(PrettyDioLogger(requestHeader: true, requestBody: true, responseHeader: true));
|
PrettyDioLogger
拦截器可以设置打印哪些信息,可根据需求进行设置。
打印效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| flutter: ╔╣ Request ║ POST flutter: ║ https: flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝ flutter: ╔ Headers flutter: ╟ content-type: application/json; charset=utf-8 flutter: ╟ Authorization: Basic ZHhtaF93ZWI6ZHhtaF93ZWJfc2VjcmV0 flutter: ╟ token: Bearer flutter: ╟ contentType: application/json; charset=utf-8 flutter: ╟ responseType: ResponseType.json flutter: ╟ followRedirects: true flutter: ╟ connectTimeout: 15000 flutter: ╟ receiveTimeout: 0 flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝ flutter: flutter: ╔╣ Response ║ POST ║ Status: 200 OK flutter: ║ https: flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝ flutter: ╔ Headers flutter: ╟ access-control-allow-credentials: [true] flutter: ╟ connection: [keep-alive] flutter: ╟ x-powered-by: [Express] flutter: ╟ set-cookie: flutter: ║ [connect.sid=s%3AkDiyUQw5crHmB0UuY03dYX3Z2HPVO8Sf.bOVO2aDh%2FSviB70e9Xt5sMQjkiDtorwn%2B%2F flutter: ║ bKN7y8UtY; Path=/; Expires=Sun, 06 Feb 2022 21:37:08 GMT; HttpOnly] flutter: ╟ date: [Sun, 06 Feb 2022 09:37:08 GMT] flutter: ╟ vary: [Accept, Origin, Accept-Encoding] flutter: ╟ content-length: [82] flutter: ╟ etag: [W/"52-2tuUsqqRy8jX+vcUJL+3D5AmQss"] flutter: ╟ content-type: [application/json; charset=utf-8] flutter: ╟ server: [nginx/1.17.8] flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝ flutter: ╔ Body flutter: ║ flutter: ║ { flutter: ║ code: 200, flutter: ║ message: "success", flutter: ║ data: {id: 111111, name: zhangsan, age: 18} flutter: ║ } flutter: ║ flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
|
loading 显示
网络请求是一个耗时操作,为了提高用户体验,一般会在请求的过程中显示 loading 提示用户正在加载数据。
前面解决异常处理使用了一个全局的 request 方法,loading 可以使用同样的思路实现,创建 loading 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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(); }
|
实现很简单,在 block 调用前后调用 loading 的 show 和 dismiss。同时对 block 包裹 try-catch 保证在异常时取消 loading,并且在 catch 中不做任何处理直接抛出异常。
这里 loading 使用了 flutter_easyloading
插件
对 request 方法进行改造支持 loading :
1 2 3 4 5 6 7 8
| 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; }
|
对 request 中的 block 又包装了一层 loading 从而实现自动 loading 的显示隐藏。
使用示例
经过上述步骤就完成了对网络请求的封装,接下来看看怎么使用。
开发过程中常用的网络请求为 get 和 post,为了方便调用,在 RequestClient
中添加 get 和 post 方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| Future<T?> get<T>( String url, { Map<String, dynamic>? queryParameters, Map<String, dynamic>? headers, bool showLoading = true, bool Function(ApiException)? onError, }) { return request(url, queryParameters: queryParameters, headers: headers, onError: onError); }
Future<T?> post<T>( String url, { Map<String, dynamic>? queryParameters, data, Map<String, dynamic>? headers, bool showLoading = true, bool Function(ApiException)? onError, }) { return request(url, method: "POST", queryParameters: queryParameters, data: data, headers: headers, onError: onError); }
|
实际也是封装后调用 request 方法。
基本使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void login(String password) => request(() async { LoginParams params = LoginParams(); params.username = "loongwind"; params.password = password; UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params); state.user = user; update(); });
Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton(onPressed: () => controller.login("123456"), child: const Text("正常登录")), ElevatedButton(onPressed: () => controller.login("654321"), child: const Text("错误登录")), Text("登录用户:${state.user?.username ?? ""}", style: TextStyle(fontSize: 20.sp),),
], )
|
自定义异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void loginError(bool errorHandler) => request(() async { LoginParams params = LoginParams(); params.username = "loongwind"; params.password = "654321"; UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params); state.user = user; print("-------------${user?.username ?? "登录失败"}"); update(); }, onError: (e){ state.errorMessage = "request error : ${e.message}"; print(state.errorMessage); update(); return errorHandler; });
|
onError 无论返回 false 或者 true 都会调用 onError 方法,且 print("-------------${user?.username ?? "登录失败"}");
这句输出并没有执行,当 onError 返回 false 时依然会弹出错误的提示,是因为返回 false 时调用了默认的异常处理弹出提示,返回 true 时则不会调用默认的异常处理方法。
在 requestClient 的请求方法上添加 onError 处理是一样的效果,不同的是在 requestClient 上的 onError 为 true 时,下面的代码会正常执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void loginError(bool errorHandler) => request(() async { LoginParams params = LoginParams(); params.username = "loongwind"; params.password = "654321"; UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params, onError: (e){ state.errorMessage = "request error : ${e.message}"; print(state.errorMessage); update(); return errorHandler; }); state.user = user; print("-------------${user?.username ?? "登录失败"}"); update(); });
|
界面效果跟上面的一样,当 onError 返回 true 时,requestClient 下面的代码会正常执行。即会打印出 -------------登录失败
, 返回 false 时则不会执行下面的代码。
loading 显示隐藏
1 2 3 4 5 6 7 8
| void loginLoading(bool showLoading) => request(() async { LoginParams params = LoginParams(); params.username = "loongwind"; params.password = "123456"; UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params, ); state.user = user; update(); }, showLoading: showLoading);
|
切换接口地址
在开发过程中会出现多个环境地址,比如开发环境、测试环境、预发布环境、生产环境等,此时为了方便切换环境一般都会在开发时增加一个环境切换的功能,此时就可以修改 baseUrl
然后重新创建 RequestClient
来实现。代码如下:
1 2
| RequestConfig.baseUrl = "https://xxxxxx"; requestClient = RequestClient();
|
源码:flutter_app_core
Flutter 应用框架搭建系列文章: