网络请求卡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,这一期就此完结。

相关推荐
某非著名程序员15 分钟前
Flutter 新手绕不过的坑:ListView 为啥顶部老有空白?
flutter·客户端
恋猫de小郭1 小时前
Google I/O Extended :2025 Flutter 的现状与未来
android·前端·flutter
九丝城主1 天前
2025使用VM虚拟机安装配置Macos苹果系统下Flutter开发环境保姆级教程--上篇
服务器·flutter·macos·vmware
瓜子三百克1 天前
七、性能优化
flutter·性能优化
恋猫de小郭2 天前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
小蜜蜂嗡嗡2 天前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
瓜子三百克2 天前
十、高级概念
flutter
帅次3 天前
Objective-C面向对象编程:类、对象、方法详解(保姆级教程)
flutter·macos·ios·objective-c·iphone·swift·safari
小蜜蜂嗡嗡3 天前
flutter flutter_vlc_player播放视频设置循环播放失效、初始化后获取不到视频宽高
flutter
孤鸿玉3 天前
[Flutter小技巧] Row中widget高度自适应的几种方法
flutter