网络请求卡UI?Flutter从Http到Dio无缝切换与封装!

Flutter从 Http 无缝过渡到 Dio

前言

熟悉我的朋友可能知道,我们项目是基于 GetX 框架搭建的,它的功能有很多,有些功能是挺好用很方便,但有些功能并不是那么好用,比如网络请求。

在之前的文章中【传送门】,我们的基于 GetX 的 GetConnect 封装的网络请求在进行 Post 请求的时候会卡 UI ,在压缩图片的过程我们移到多线程处理,但是在流的上传这一过程中还是会造成卡顿。

当时我们说到可以用原生桥接的方式,这也是大厂都用的方案,而另一种方案就是 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 并初始化全局的一些属性:

php 复制代码
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 网络请求入口

dart 复制代码
  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,更方便。

php 复制代码
 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,并设置到对应的网络请求,页面关闭的时候直接调用取消整个页面的网络请求。

javascript 复制代码
  /*
   * 取消请求
   */
  void cancelDio(CancelToken token) {
    token.cancel("cancelled");
  }

确实可以很方便的避免一些内存泄露与无效的请求。

关于封装的优化建议:使用 compute 对 io 或编解码的过程进行多线程操作,比如缓存,比如大对象Json的序列化等等。如果操作不当也会造成 UI 的卡顿哦。

Demo还没来得及做,关键代码都已经贴出,配合之前的 Http 封装来修改的,后续出 Demo 了在重新更新(主要还是比较菜只开始搞Flutter2个月而已)。

如果代码、注释、理解有不到位或错漏的地方,或者有不同意见的,希望同学们可以在评论区指出或交流。

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

相关推荐
Flutter OH3 小时前
Flutter OH平台 DFX 问题定位导航
flutter
恋猫de小郭5 小时前
AI 时代,也许你的 Flutter 需要一套 Dartastic OpenTelemetry 监控
android·前端·flutter
等....6 小时前
Flutter创建
flutter
Privasa-隐私实验室6 小时前
跨端技术选型|Flutter搭建隐私加密App,安卓与iOS双端差异适配实战
android·笔记·安全·flutter·ios·隐私安全·aes-256
Flutter OH16 小时前
Flutter OH 外接纹理问题定位指南
flutter
未来猫咪花21 小时前
Everything is ViewModel:让状态管理回到对象世界
android·flutter·ios
Flutter OH21 小时前
Flutter OH 日志抓取与过滤指南
flutter
Flutter OH1 天前
Flutter OH 内存与 GPU 问题定位指南
flutter
Flutter OH1 天前
Flutter OH 多设备适配问题定位指南
flutter
律宏阔1 天前
Flutter Hooks 与 flutter_map_animations 冲突:第二次地图动画报错的解决方案
flutter