likes
comments
collection
share

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

作者站长头像
站长
· 阅读数 32

Flutter从 Http 无缝过渡到 Dio

前言

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

当时我们说到可以用原生桥接的方式,这也是大厂都用的方案,而另一种方案就是 Dio ,也是 Flutter App 常用的网络请求框架。

本文就基于之前 Http 请求方式的封装,换到 Dio 的方式来,由于我们早期规划以及把网络请求层作为功能引擎隔离开了,现在项目已经做完了,要替换全局的网络请求,我们只需要更换网络引擎即可。

下面就开始吧。

一、封装

关于 GetConnect 的 Http 请求封装在我之前的文章中有过很多分享,例如如何封装返回参数,如何处理缓存策略,如何处理拦截器与日志等等,关于它的封装有兴趣可以往前翻翻。

这里我们直接放一张图,说一下大致的实现。

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

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 查看文档,非常的清晰。

拦截器的定义:

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

里面很多业务逻辑代码就折叠了,主要是用于拦截网络请求修改或添加一些固定的请求头。

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

这个拦截器也是业务逻辑,对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 可以根据拦截器做缓存,但是我之前都已经用这种方式了,懒得改,效果是一样的)

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

由于这里和之前的 Http 封装类似,只是具体发起请求的地方换成了 Dio 而已,直接偷懒贴图了。

封装完成之后使用的时候把数据仓库源一换即可,其他无需变动:

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

二、测试

可以看到换到 Dio 之后,使用 Dio 自带的 Log 拦截器打印的日志如下:

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

使用 Log 打印成功请求的内容如下:

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

缓存策略存入与取出缓存:

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

拦截器登录令牌失效的处理:

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

Dio 的 Post 请求文件效果(特意选大图并特意不压缩并特意选用低性能手机测试):

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

图片已经上传给后端并返回了结果:

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

可以看到不会再卡 UI 了,那么到此我们就集成换装完毕了。

总结

虽然 GetX 自带 Http 网络请求,不想用 Dio 增大包体积导入重复的功能。但是它自带的网络请求框架确实不是那么好用。

但是如果是对于 Flutter App 来说 Dio 是值得的,并且它的体积也并不大,32位或64位包体积增大40K,32+64全量包增大大概80K,使用体验确实是比 Http 要好。

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

至于说 Dio 比 原生 Http 要请求更快?那也是大可不必这么说,只是体验好一些、功能多一些罢了。

比如我们现在可以愉快的监听下载的进度和上传图片的进度了。

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

比如我们现在可以很方便的在页面销毁的时候,GetXController销毁的 close 回调中取消整个页面的网络请求,设置一个CancelToken,并设置到对应的网络请求,页面关闭的时候直接调用取消整个页面的网络请求。

  /*
   * 取消请求
   */
  void cancelDio(CancelToken token) {
    token.cancel("cancelled");
  }

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

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

如果想要源码也可以到我的 Flutter Demo 查看源码【传送门】

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

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

Ok,这一期就此完结。

【Flutter】如何解决网络请求卡UI?从Http到Dio无缝切换与封装

转载自:https://juejin.cn/post/7300929241949044771
评论
请登录