应用开发中,网络请求几乎是必不可少的功能,本文将介绍如何通过对 dio
进行二次封装一步一步实现网络请求封装,以便于在项目中方便快捷的使用网络请求。
封装后的网络请求将具备如下功能:
•简单易用•数据解析•异常处理•请求拦截•日志打印• loading 显示
下面将一步一步带你实现网络请求的封装。
添加依赖
首先在项目里添加 dio
的依赖:
dependencies:
dio: ^4.0.4
请求封装
首先创建一个 RequestConfig
类,用于放置 dio
的配置参数,如下:
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)
);
}
}
在类的上方,创建了一个全局的变量 requestClient
方便外部调用。
dio
本身提供了get
、post
、put
、delete
等一系列 http 请求方法,但是通过源码发现最终这些方法都是调用的 request
的方法实现的。所以这里直接对 dio 的 request
方法进行封装。
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 方法里进行统一的数据处理,如数据解析等。
数据解析
返回数据解析
在移动开发中,开发者习惯将返回数据解析成实体类使用,接下来将介绍如何结合 dio 完成数据解析的封装。
项目开发中接口返回的数据结构一般是这样的:
代码语言:javascript复制{
"code": 200,
"message": "success",
"data":{
"id": "12312312",
"name": "loongwind",
"age": 18
}
}
创建 ApiResponse
类用于解析接口返回数据:
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);
}
}
因为返回的数据中 data 的数据类型是不定的,所以改造 request
支持泛型,然后在 request 方法中统一进行数据解析,然后返回 data 数据,代码如下:
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
类,用于解析原始数据:
class RawData{
dynamic value;
}
然后修改 RequestClient
中的 _handleResponse
:
///请求响应内容处理
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 数据再进行数据请求。
代码语言:javascript复制 _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
用于统一封装请求的异常信息:
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 主要根据 DioError 信息创建 ApiException,但是仔细发现其中有一段解析返回数据让创建 ApiException 的代码,如下:
代码语言:javascript复制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 方法改造如下:
代码语言:javascript复制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 不为空进行后续处理,如果一个业务存在多个请求依赖调用,则此处则会嵌套多次,代码阅读性不好。如下:
代码语言:javascript复制var data1 = requestClient.request(url1);
if( data1 != null ){
var data2 = requestClient.request(url2);
if(data2 != null){
var data3 = requestClient.request(url3);
///...
}
}
为了解决上述问题,并且实现统一异常处理,创建一个顶级的 request 方法:
代码语言:javascript复制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 则跳转登录页,其他错误统一弹出错误提示。
此时使用如下:
代码语言:javascript复制 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 信息时就可以通过拦截器实现。
代码语言:javascript复制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 时添加拦截器即可:
代码语言:javascript复制_dio.interceptors.add(TokenInterceptor());
日志打印
开发过程中为了方便调试经常需要打印请求返回日志,可以使用自定义拦截器实现,也可以使用第三方实现的日志打印的拦截器 pretty_dio_logger
库。
添加依赖:
代码语言:javascript复制pretty_dio_logger: ^1.1.1
dio 添加日期拦截器:
代码语言:javascript复制_dio.interceptors.add(PrettyDioLogger(requestHeader: true, requestBody:
true, responseHeader: true));
PrettyDioLogger
拦截器可以设置打印哪些信息,可根据需求进行设置。
打印效果:
代码语言:javascript复制flutter: ╔╣ Request ║ POST
flutter: ║ https://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test/test
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://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test/test
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:kDiyUQw5crHmB0UuY03dYX3Z2HPVO8Sf.bOVO2aDh/SviB70e9Xt5sMQjkiDtorwn+/
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 方法:
代码语言:javascript复制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 :
代码语言:javascript复制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 方法,如下:
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 方法。
基本使用
代码语言:javascript复制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();
});
/// View
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Text("${SR.hello.tr} : ${state.count}", style: TextStyle(fontSize: 50.sp),),
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),),
],
)
自定义异常处理
代码语言:javascript复制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 时,下面的代码会正常执行:
代码语言:javascript复制 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 显示隐藏
代码语言:javascript复制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
来实现。代码如下:
RequestConfig.baseUrl = "https://xxxxxx";
requestClient = RequestClient();
源码:flutter_app_core[1]
References
[1]
flutter_app_core: https://github.com/loongwind/flutter_app_core