【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装
Flutter从 Http 无缝过渡到 Dio
前言
熟悉我的朋友可能知道,我们项目是基于 GetX 框架搭建的,它的功能有很多,有些功能是挺好用很方便,但有些功能并不是那么好用,比如网络请求。
当时我们说到可以用原生桥接的方式,这也是大厂都用的方案,而另一种方案就是 Dio ,也是 Flutter App 常用的网络请求框架。
本文就基于之前 Http 请求方式的封装,换到 Dio 的方式来,由于我们早期规划以及把网络请求层作为功能引擎隔离开了,现在项目已经做完了,要替换全局的网络请求,我们只需要更换网络引擎即可。
下面就开始吧。
一、封装
关于 GetConnect 的 Http 请求封装在我之前的文章中有过很多分享,例如如何封装返回参数,如何处理缓存策略,如何处理拦截器与日志等等,关于它的封装有兴趣可以往前翻翻。
这里我们直接放一张图,说一下大致的实现。
requestNetResult 是我自定义的网络请求唯一入口,常用的参数都统一封装了,在内部处理了缓存策略的存入,处理了成功的数据返回与失败的数据,把 Htpp 的 Response 封装为自定义的 HttpResult 并返回给数据仓库处理。
_formatHttpErrorMessage 是对错误的处理,格式化文本方便上层调用或展示。
_generateRequest 根据Get与Post方法,生成对应的Response,如果有缓存策略则会直接返回缓存数据。
_generateKeyByUrlParams 是根据Get请求的url与参数生成对应的key。
那么如何换成 Dio 的方式呢?用一样的方法名即可。
如果按正常的策略模式来说应该是用接口定义方法,然后切换使用不同的策略/引擎,我这里偷懒就直接写了。
1.1 创建 DioProvider 初始化
我们先创建自己的 DioProvider 并初始化全局的一些属性:
class DioProvider {
late Dio dio;
DioProvider() {
final options = BaseOptions(
baseUrl: ApiConstants.baseUrl,
connectTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
validateStatus: (code) {
//指定这些HttpCode都算成功
if (code == 200 || code == 401 || code == 422 || code == 429) {
return true;
} else {
return false;
}
});
dio = Dio(options);
dio.interceptors.add(AuthDioInterceptors()); //处理请求之前的请求头
dio.interceptors.add(StatusCodeDioInterceptors()); //处理响应之后的状态码
if (!AppConstant.inProduction) {
dio.interceptors.add(LogInterceptor(responseBody: false));
}
}
这些方法都是 Dio 的 Api ,如果不了解可以去 github 查看文档,非常的清晰。
拦截器的定义:
里面很多业务逻辑代码就折叠了,主要是用于拦截网络请求修改或添加一些固定的请求头。
这个拦截器也是业务逻辑,对Token失效,网络成功之后的缓存策略做处理。
1.2 网络请求入口
Future<HttpResult> requestNetResult(
String url, {
HttpMethod? method, //指明Get还是Post请求
Map<String, String>? headers, //请求头
Map<String, dynamic>? params, //请求参数,Get的Params,Post的Form
Map<String, String>? paths, //文件Flie
Map<String, Uint8List>? pathStreams, //文件流
CacheControl? cacheControl, // Get请求是否需要缓存
Duration? cacheExpiration, //缓存是否需要过期时间,过期时间为多长时间
ProgressCallback? send, // 上传进度监听
ProgressCallback? receive, // 下载监听
CancelToken? cancelToken, // 用于取消的 token,可以多个请求绑定一个 token
}) async {
try {
//根据参数封装请求并开始请求
Response response;
if (!AppConstant.inProduction) {
final startTime = DateTime.now();
response = await _generateRequest(
method,
params,
paths,
pathStreams,
url,
headers,
cacheControl,
cacheExpiration,
send,
receive,
cancelToken,
);
final endTime = DateTime.now();
final duration = endTime.difference(startTime).inMilliseconds;
Log.d('网络请求耗时 $duration 毫秒, 响应内容 ${response.data}}');
} else {
response = await _generateRequest(
method,
params,
paths,
pathStreams,
url,
headers,
cacheControl,
cacheExpiration,
send,
receive,
cancelToken,
);
}
//判断成功与失败, 200 成功 401 授权过期, 422 请求参数错误,429 请求太频繁
if (response.statusCode == 200 || response.statusCode == 401 || response.statusCode == 422 || response.statusCode == 429) {
//网络请求完成之后获取正常的Json-Map
Map<String, dynamic> jsonMap = response.data;
//先处理缓存逻辑
Map<String, dynamic> extraMap = response.extra;
final cacheControl = extraMap['cache_control'];
if (cacheControl != null) {
final cacheKey = extraMap['cache_key'];
final cacheExpiration = extraMap['cache_expiration'];
Log.d('response 中携带缓存处理逻辑 cacheControl ==== > $cacheControl cacheKey ==== > $cacheKey cacheExpiration ==== > ' '$cacheExpiration');
Duration? duration;
if (cacheExpiration != null) {
duration = Duration(milliseconds: int.parse(cacheExpiration));
}
//直接存入原生Json数据
fileCache.putJsonByKey(
cacheKey ?? 'unknow',
jsonMap,
expiration: duration,
);
}
... 后面是数据处理的逻辑,太多了,重复的,和上面的GetConnect封装的处理一样
}
由于我们设置了HttpCode 的 200 401 422 429 这些都能走到成功的回调,这个其实是看各自服务端的定义,我们的服务端是走的错误回调,比如输入账号密码登录,如果密码错误会返回 422,而很多服务端会返回 200 也就是网络请求成功,只是业务逻辑错误返回的数据 code 是 422,而我们是 HttpCode 就是422,如果不处理就会走到错误的回调中,这点需要注意。
接下来处理了缓存的策略处理,有存入缓存的需求就会处理对应的逻辑。
1.3 根据请求方式生成请求体
_generateRequest 方法就是根据请求方式生成请求体,我们这里额外加入了进度的回调与取消的Token,更方便。
if (method != null && method == HttpMethod.POST) {
var map = <String, dynamic>{};
if (params != null || paths != null || pathStreams != null) {
//只要有一个不为空,就可以封装参数
//默认的参数
if (params != null) {
map.addAll(params);
}
//Flie文件
if (paths != null && paths.isNotEmpty) {
for (final entry in paths.entries) {
final key = entry.key;
final value = entry.value;
if (value.isNotEmpty && RegCheckUtils.isLocalImagePath(value)) {
// 以文件的方式压缩,获取到流对象
Uint8List? stream = await FlutterImageCompress.compressWithFile(
value,
minWidth: 1000,
minHeight: 1000,
quality: 80,
);
//传入压缩之后的流对象
if (stream != null) {
map[key] = MultipartFile.fromBytes(stream, filename: "file");
}
}
}
}
//File文件流
if (pathStreams != null && pathStreams.isNotEmpty) {
for (final entry in pathStreams.entries) {
final key = entry.key;
final value = entry.value;
if (value.isNotEmpty) {
// 以流方式压缩,获取到流对象
Uint8List stream = await FlutterImageCompress.compressWithList(
value,
minWidth: 1000,
minHeight: 1000,
quality: 80,
);
//传入压缩之后的流对象
map[key] = MultipartFile.fromBytes(stream, filename: "file_stream");
}
}
}
}
final formData = FormData.fromMap(map);
if (!AppConstant.inProduction) {
print('Post请求FromData参数,fields:${formData.fields.toString()} files:${formData.files.toString()}');
}
//以 Post-FromData 的方式上传
req = dio.post(
url,
data: formData,
options: Options(headers: headers),
onSendProgress: send,
onReceiveProgress: receive,
cancelToken: cancelToken,
);
}
与 Http 的请求类似,我们也是使用 FromData 的方式进行 Post 请求,只是这里 MultipartFile 的获取换为了 Dio 的方式。
而 Get 请求由于我们要处理缓存,还是和之前的逻辑类似,直接返回自定义的 Response 。(其实 Dio 可以根据拦截器做缓存,但是我之前都已经用这种方式了,懒得改,效果是一样的)
由于这里和之前的 Http 封装类似,只是具体发起请求的地方换成了 Dio 而已,直接偷懒贴图了。
封装完成之后使用的时候把数据仓库源一换即可,其他无需变动:
二、测试
可以看到换到 Dio 之后,使用 Dio 自带的 Log 拦截器打印的日志如下:
使用 Log 打印成功请求的内容如下:
缓存策略存入与取出缓存:
拦截器登录令牌失效的处理:
Dio 的 Post 请求文件效果(特意选大图并特意不压缩并特意选用低性能手机测试):
图片已经上传给后端并返回了结果:
可以看到不会再卡 UI 了,那么到此我们就集成换装完毕了。
总结
虽然 GetX 自带 Http 网络请求,不想用 Dio 增大包体积导入重复的功能。但是它自带的网络请求框架确实不是那么好用。
但是如果是对于 Flutter App 来说 Dio 是值得的,并且它的体积也并不大,32位或64位包体积增大40K,32+64全量包增大大概80K,使用体验确实是比 Http 要好。
至于说 Dio 比 原生 Http 要请求更快?那也是大可不必这么说,只是体验好一些、功能多一些罢了。
比如我们现在可以愉快的监听下载的进度和上传图片的进度了。
比如我们现在可以很方便的在页面销毁的时候,GetXController销毁的 close 回调中取消整个页面的网络请求,设置一个CancelToken,并设置到对应的网络请求,页面关闭的时候直接调用取消整个页面的网络请求。
/*
* 取消请求
*/
void cancelDio(CancelToken token) {
token.cancel("cancelled");
}
确实可以很方便的避免一些内存泄露与无效的请求。
关于封装的优化建议:使用 compute 对 io 或编解码的过程进行多线程操作,比如缓存,比如大对象Json的序列化等等。如果操作不当也会造成 UI 的卡顿哦。
如果想要源码也可以到我的 Flutter Demo 查看源码【传送门】 。
如果代码、注释、理解有不到位或错漏的地方,或者有不同意见的,希望同学们可以在评论区指出或交流。
如果感觉本文对你有一点点的启发,还望你能点赞
支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。
转载自:https://juejin.cn/post/7300929241949044771