【Flutter】拆分解耦网络引擎、请求缓存、请求去重,这是你想要的 Dio 封装吗?

拆分网络引擎、请求缓存拦截、请求去重拦截之后的完整 Dio 封装

前言

前段时间看见有网友在交流群中寻找 Dio 的封装,结果得到的回答就是格局小了, 完美的 Dio 需要封装吗?

啊? 这 ... 我不敢大声说话了。

难道大家使用网络请求都不封装的吗?哪里要用直接在那里初始化对象,拼装参数,处理结果与异常吗?

那不得乱到飞起...,一旦有修改难道要去每一个页面一个一个实例去找吗?

当然也可能是大佬跟我们开个小小的玩笑,小弟虽然入门 Flutter 时间不长,也看过几个开源项目,基本上都是有 DIO 网络请求的封装的。

个人理解中比较好的网络请求封装应该是这样:

  1. 集中的单例的管理网络请求实例,避免多实例浪费内存。

  2. 抽取网络请求引擎,方便后期更换网络请求框架。

  3. 封装简化请求参数,简化请求流程,方便添加公共逻辑。

  4. 可用拦截器的方式抽离各项子功能,面向切面编程。

在之前的文章中我们已经详细的介绍了 Dio 的基本请求功能封装,缓存处理,去重拦截等逻辑,如果有兴趣可以查询之前的文章。

为什么要用 Dio 网络框架

如何替换到 Dio 网络框架

如何使用文件缓存

如何在 Dio 中加入网络请求缓存策略

如何在 Dio 中实现网络防抖请求去重

功能全部整理之后导致我的网络工具类有 1000 多行代码了,主要是太臃肿,其次是很多开发者并不需要这么多的功能。

因为想要做到灵活配置,所以才有本期文章拆分网络引擎,拆分网络请求缓存策略,拆分请求去重逻辑,使用拦截器的方式实现,完全可以达到可配置的效果,需要哪个功能加哪个功能,按需加载对应拦截器相对更灵活。

OK,话不多说,下面就开始吧。

一、拆分网络引擎

我们一般使用多媒体,相机相册,权限,裁剪,弹窗,气泡等常用功能都是使用引擎类封装,App只管调用相关的方法实现功能,无需关心内部的实现,引擎内部根据不同的依赖去实现具体的逻辑,一旦版本变化,Api变化我们也只需要修改引擎类即可,无需每一处调用都去修改,极大的提高维护效率。(防御性编程除外)

而网络请求的引擎类也是如此,调用者只关心 Get 请求 和 Post 请求发出了给我响应,你是哪种网络请求框架它并不关心,所以我们先抽取 Dio 最基本的请求作为一个引擎类:

dart 复制代码
/*
 * 网络请求引擎封装,目前使用的是 Dio 框架
 */
class NetworkEngine {
  late Dio dio;

  NetworkEngine() {
    /// 网络配置
    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的转换器
    dio.transformer = BackgroundTransformer(); //Json后台线程处理优化(可选)

    // 设置Dio的拦截器
    dio.interceptors.add(NetworkDebounceInterceptor()); //处理网络请求去重逻辑
    dio.interceptors.add(AuthDioInterceptors()); //处理请求之前的请求头(项目业务逻辑)
    dio.interceptors.add(StatusCodeDioInterceptors()); //处理响应之后的状态码(项目业务逻辑)
    dio.interceptors.add(CacheControlnterceptor()); //处理 Http Get 请求缓存策略
    if (!AppConstant.inProduction) {
      dio.interceptors.add(LogInterceptor(responseBody: false)); //默认的 Dio 的 Log 打印
    }
  }

  /// 网络请求 Post 请求
  Future<Response> executePost({
    required String url,
    Map<String, dynamic>? params,
    Map<String, String>? paths, //文件
    Map<String, Uint8List>? pathStreams, //文件流
    Map<String, String>? headers,
    ProgressCallback? send, // 上传进度监听
    ProgressCallback? receive, // 下载监听
    CancelToken? cancelToken, // 用于取消的 token,可以多个请求绑定一个 token
  }) async {
    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()}');
    }

    return dio.post(
      url,
      data: formData,
      options: Options(headers: headers),
      onSendProgress: send,
      onReceiveProgress: receive,
      cancelToken: cancelToken,
    );
  }

  /// 网络请求 Get 请求
  Future<Response> executeGet({
    required String url,
    Map<String, dynamic>? params,
    Map<String, String>? headers,
    CacheControl? cacheControl,
    Duration? cacheExpiration,
    ProgressCallback? receive, // 请求进度监听
    CancelToken? cancelToken, // 用于取消的 token,可以多个请求绑定一个 token
  }) {
    return dio.get(
      url,
      queryParameters: params,
      options: Options(headers: headers),
      onReceiveProgress: receive,
      cancelToken: cancelToken,
    );
  }

  /// Dio 网络下载
  Future<void> downloadFile({
    required String url,
    required String savePath,
    ProgressCallback? receive, // 下载进度监听
    CancelToken? cancelToken, // 用于取消的 token,可以多个请求绑定一个 token
    void Function(bool success, String path)? callback, // 下载完成回调函数
  }) async {
    try {
      await dio.download(
        url,
        savePath,
        onReceiveProgress: receive,
        cancelToken: cancelToken,
      );
      // 下载成功
      callback?.call(true, savePath);
    } on DioException catch (e) {
      Log.e("DioException:$e");
      // 下载失败
      callback?.call(false, savePath);
    }
  }
}

如果你的项目,成功 Code 或者 baseUrl 或者超时时间等配置需要修改,直接这里修改即可,如果你的请求还有 Delete 等其他的请求方式,也可以自行添加类似的请求。

二、拆分缓存逻辑到拦截器

在网络引擎的封装中,我们可以看到一件默认添加了很多拦截器,一些是我们业务逻辑的拦截器,比如用户加密,请求加密解密,通行令牌,错误弹窗等逻辑,我们这里关心的就是请求缓存与请求去重这两个公共的拦截器逻辑。

请求缓存的拦截器如下:

ini 复制代码
/*
 * Http的缓存策略与处理
 */
class CacheControlnterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    Map<String, dynamic> headers = options.headers;
    final String cacheControlName = headers['cache_control'] ?? "";

    //只缓存
    if (cacheControlName == CacheControl.onlyCache.name) {
      final key = options.uri.toString();
      //直接返回缓存
      final json = await FileCacheManager().getJsonByKey(key);
      if (json != null) {
        handler.resolve(Response(
          statusCode: 200,
          data: json,
          statusMessage: '获取缓存成功',
          requestOptions: RequestOptions(),
        ));
      } else {
        handler.resolve(Response(
          statusCode: 200,
          data: json,
          statusMessage: '获取网络缓存数据失败',
          requestOptions: RequestOptions(),
        ));
      }

      //有缓存用缓存,没缓存用网络请求的数据并存入缓存
    } else if (cacheControlName == CacheControl.cacheFirstOrNetworkPut.name) {
      final key = options.uri.toString();
      final json = await FileCacheManager().getJsonByKey(key);
      if (json != null) {
        handler.resolve(Response(
          statusCode: 200,
          data: json,
          statusMessage: '获取缓存成功',
          requestOptions: RequestOptions(),
        ));
      } else {
        //处理数据缓存需要的请求头
        headers['cache_key'] = key;
        options.headers = headers;
        //继续转发,走正常的请求
        handler.next(options);
      }

      //用网络请求的数据并存入缓存
    } else if (cacheControlName == CacheControl.onlyNetworkPutCache.name) {
      final key = options.uri.toString();
      //处理数据缓存需要的请求头
      headers['cache_key'] = key;
      options.headers = headers;
      //继续转发,走正常的请求
      handler.next(options);

      //不满足条件不需要拦截
    } else {
      handler.next(options);
    }
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    if (response.statusCode == 200) {
      //成功的时候设置缓存数据放入 headers 中
      //响应体中请求体的请求头数据
      final Map<String, dynamic> requestHeaders = response.requestOptions.headers;

      if (requestHeaders['cache_control'] != null) {
        final cacheKey = requestHeaders['cache_key'];
        final cacheControlName = requestHeaders['cache_control'];
        final cacheExpiration = requestHeaders['cache_expiration'];

        //网络请求完成之后获取正常的Json-Map
        Map<String, dynamic> jsonMap = response.data;

        Log.d('response 中携带缓存处理逻辑 cacheControl ==== > $cacheControlName '
            'cacheKey ==== > $cacheKey cacheExpiration ==== > $cacheExpiration');

        Duration? duration;
        if (cacheExpiration != null) {
          duration = Duration(milliseconds: int.parse(cacheExpiration));
        }

        //直接存入Json数据到本地File
        fileCache.putJsonByKey(
          cacheKey ?? 'unknow',
          jsonMap,
          expiration: duration,
        );
      }
    }

    super.onResponse(response, handler);
  }

  @override
  Future onError(DioException err, ErrorInterceptorHandler handler) async {
    super.onError(err, handler);
  }
}

本质还是之前缓存策略的逻辑,如果有疑惑的可以看看前文的链接,之前都有具体的实现步骤。

主要是在请求的拦截中判断逻辑是否要返回缓存,或者添加请求头发起请求,在拦截的响应中判断逻辑是否需要存入缓存。

三、拆分请求去重逻辑到拦截器

去重的拦截器逻辑相对比较复杂,先上完整代码:

ini 复制代码
/*
 * Dio 网络请求去重的拦截器
 */
class NetworkDebounceInterceptor extends Interceptor {
  static final Map<String, CancelToken> _cancelTokenMap = {}; // 保存每个请求的 CancelToken
  static final Map<String, String> _urlParamsMap = {}; // 保存每个请求的url与params的序列化对应关系

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    Map<String, dynamic>? parameters;
    final url = options.uri.path;
    final method = options.method;
    CancelToken? cancelToken = options.cancelToken;
    Map<String, dynamic> headers = options.headers;
    final isShowLoadingDialog = headers['is_show_loading_dialog'] != null && headers['is_show_loading_dialog'] == 'true';

    if (headers['network_debounce'] != null && headers['network_debounce'] == 'true') {
      //需要处理请求防抖去重
      parameters = _generateParameters(method, options);
      _handleNetworkDebounce(url, method, parameters, cancelToken, options, handler, isShowLoadingDialog);
    } else {
      if (isShowLoadingDialog) {
        SmartDialog.showLoading();
      }
      // 处理去重
      super.onRequest(options, handler);
    }
  }

  //根据请求方式生成不同的参数格式
  Map<String, dynamic>? _generateParameters(String method, RequestOptions options) {
    if (method == 'GET') {
      return options.queryParameters;
    } else if (method == 'POST' && options.data is FormData) {
      final formData = options.data as FormData;
      //临时Map
      final Map<String, dynamic> map = {};
      // 添加 formData.fields 到映射中
      for (var field in formData.fields) {
        map[field.key] = field.value;
      }
      // 添加 formData.files 的键和文件长度到映射中
      for (var file in formData.files) {
        Log.d("当前文件 key:${file.key} lenght:${file.value.length}");
        map[file.key] = file.value.length;
      }
      return map;
    } else {
      return null;
    }
  }

  //添加CancelToken的逻辑
  void addCancelToken(
    String url,
    String method,
    Map<String, dynamic>? params,
    CancelToken? cancelToken,
    RequestOptions options,
  ) {
    final cancelKey = _generateCacelKey(method, url, params);
    cancelToken ??= CancelToken();
    _cancelTokenMap[cancelKey] = cancelToken;
    options.cancelToken = cancelToken;
  }

  //移除CancelToken的逻辑
  void removeCancelToken(
    String url,
    String method,
    Map<String, dynamic>? params,
  ) {
    //自动添加CancelToken的逻辑
    final cancelKey = _generateCacelKey(method, url, params);
    if (_cancelTokenMap[cancelKey] != null) {
      _cancelTokenMap.remove(cancelKey);
    }
  }

  void _handleNetworkDebounce(
    String url,
    String method,
    Map<String, dynamic>? params,
    CancelToken? cancelToken,
    RequestOptions options,
    RequestInterceptorHandler handler,
    bool isShowLoadingDialog,
  ) async {
    if (params == null) {
      //无需处理去重
      if (isShowLoadingDialog) {
        SmartDialog.showLoading();
      }
      handler.next(options);
    } else {
      addCancelToken(url, method, params, cancelToken, options);
      //加CancelToken之后
      //拿到当前的url对应的Map中的数据
      final urlkey = _generateKeyByMethodUrl(method, url);
      Log.d("请求前先查询 _urlParamsMap 集合目前的数据:${_urlParamsMap.toString()} _cancelTokenMap:${_cancelTokenMap.toString()}");
      final preSerializedParams = _urlParamsMap[urlkey];
      final curSerializedParams = _serializeAllParams(params);
      Log.d("已缓存的请求参数Value cacheedValue:${preSerializedParams.toString()} 当前正在发起的请求的参数Value:${curSerializedParams.toString()}");

      if (preSerializedParams == null) {
        //说明没有缓存,添加缓存
        _urlParamsMap[urlkey] = curSerializedParams;
        //正常请求
        if (isShowLoadingDialog) {
          SmartDialog.showLoading();
        }
        handler.next(options);
      } else {
        //有缓存对比
        if (curSerializedParams == preSerializedParams) {
          //如果两者相同,说明是重复的请求,舍弃当前的请求
          Log.d("是重复的请求,舍弃当前的请求");
          handler.resolve(Response(
            statusCode: ApiConstants.networkDebounceCode,
            statusMessage: 'Request canceled',
            requestOptions: RequestOptions(),
          ));
        } else {
          //如果两者不相,说明不是重复的请求,需要取消之前的网络请求,发起新的请求
          Log.d("不是重复的请求,需要取消之前的网络请求,发起新的请求");
          //拿到当前请求的cancelToken
          final previousCancekKey = "$urlkey - $preSerializedParams";
          final previousCancelToken = _cancelTokenMap[previousCancekKey];
          if (previousCancelToken != null) {
            previousCancelToken.cancel('Request canceled');
            _urlParamsMap.remove(urlkey);
            _cancelTokenMap.remove(previousCancekKey);
          }

          //添加缓存
          _urlParamsMap[urlkey] = curSerializedParams;

          //加CancelToken之后正常请求
          addCancelToken(url, method, params, cancelToken, options);
          handler.next(options);
        }
      }
    }
  }

  //根据请求方式和Url生成Key
  String _generateKeyByMethodUrl(String method, String url) {
    return "$method - $url";
  }

  //CancelToken Map 的 Key 生成
  String _generateCacelKey(String method, String url, Map<String, dynamic>? map) {
    return "${_generateKeyByMethodUrl(method, url)} - ${_serializeAllParams(map)}";
  }

  //参数序列化为唯一字符串
  String _serializeAllParams(Map<String, dynamic>? map) {
    if (map == null || map.isEmpty) {
      return '';
    }
    return map.toString();
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    handleEndWithRequestOption(response.requestOptions);
    super.onResponse(response, handler);
  }

  void handleEndWithRequestOption(RequestOptions requestOptions) {
    final Map<String, dynamic> requestHeaders = requestOptions.headers;
    final isShowLoadingDialog = requestHeaders['is_show_loading_dialog'] != null && requestHeaders['is_show_loading_dialog'] == 'true';

    if (requestHeaders['network_debounce'] != null && requestHeaders['network_debounce'] == 'true') {
      //请求完成之后移除CancelToken,和 Params Map
      final url = requestOptions.uri.path;
      final method = requestOptions.method;
      Map<String, dynamic>? params = _generateParameters(method, requestOptions);

      final urlkey = _generateKeyByMethodUrl(method, url);
      _urlParamsMap.remove(urlkey);

      removeCancelToken(url, method, params);

      Log.d("网络请求去重的全部流程完成,移除Map内存缓存完成");
    }

    if (isShowLoadingDialog) {
      SmartDialog.dismiss(status: SmartStatus.loading);
    }
  }

  @override
  Future onError(DioException err, ErrorInterceptorHandler handler) async {
    //请求错误也需要处理清除缓存和Loading的逻辑
    handleEndWithRequestOption(err.requestOptions);
    super.onError(err, handler);
  }
}

具体实现的步骤还是和之前的思路一致,如果有疑惑的可以看看前文的链接查看网络去重的思路。

这里的主要逻辑是在请求的拦截中判断逻辑是否当前已经有同样的请求真正进行中,对比序列化之后的参数是舍弃当前请求还是取消之前的请求,在拦截的响应中处理完成的逻辑。

需要注意的是网络请求不可能没有错误,我们需要处理异常的信息,也需要对缓存的请求做出处理,完整代码中已经比较完善的注释和Log,还是很方便查阅的。

最后我们还加入了 Loading Dialog 的控制,可以在需要网络防抖去重的场景下避免多次 Loading Dialog 的尴尬场景。

四、完整代码

最后我们的 Http 工具类就从 1000 多行缩减到现在的 200 多行,并且其中的大部分逻辑都是数据的处理和异常的处理。这也是可能需要你修改的地方,因为你不可能和我们的项目完全一样的数据处理和异常处理。

完整的代码如下:

kotlin 复制代码
enum CacheControl {
  noCache, //不使用缓存
  onlyCache, //只用缓存
  cacheFirstOrNetworkPut, //有缓存先用缓存,没有缓存进行网络请求再存入缓存
  onlyNetworkPutCache, //只用网络请求,但是会存入缓存
}

//项目中只用到了Get Post 两种方式
enum HttpMethod { GET, POST }

///Dio封装管理,网络请求引擎类
class HttpProvider {
  //具体的执行网络请求逻辑在引擎类中
  final networkEngine = NetworkEngine();

  /// 封装网络请求入口
  Future<HttpResult> requestNetResult(
    String url, {
    HttpMethod method = HttpMethod.GET, //指明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
    bool networkDebounce = false, // 当前网络请求是否需要网络防抖去重
    bool isShowLoadingDialog = false, // 是否展示 Loading 弹框
  }) async {
    //尝试网络请求去重,内部逻辑判断发起真正的网络请求
    if (networkDebounce) {
      if (headers == null || headers.isEmpty) {
        headers = <String, String>{};
      }
      headers['network_debounce'] = "true";
    }

    if (isShowLoadingDialog) {
      if (headers == null || headers.isEmpty) {
        headers = <String, String>{};
      }
      headers['is_show_loading_dialog'] = "true";
    }

    return _executeRequests(
      url,
      method,
      headers,
      params,
      paths,
      pathStreams,
      cacheControl,
      cacheExpiration,
      send,
      receive,
      cancelToken,
      networkDebounce,
    );
  }

  /// 真正的执行请求,处理缓存与返回的结果
  Future<HttpResult> _executeRequests(
    String url, //请求地址
    HttpMethod method, //请求方式
    Map<String, String>? headers, //请求头
    Map<String, dynamic>? params, //请求参数
    Map<String, String>? paths, //文件
    Map<String, Uint8List>? pathStreams, //文件流
    CacheControl? cacheControl, //Get请求缓存控制
    Duration? cacheExpiration, //缓存文件有效时间
    ProgressCallback? send, // 上传进度监听
    ProgressCallback? receive, // 下载监听
    CancelToken? cancelToken, // 用于取消的 token,可以多个请求绑定一个 token
    bool networkDebounce, // 当前网络请求是否需要网络防抖去重
  ) async {
    try {
      //根据参数封装请求并开始请求
      Response response;

      // 定义一个局部函数,封装重复的请求逻辑
      Future<Response> executeGenerateRequest() async {
        return _generateRequest(
          method,
          params,
          paths,
          pathStreams,
          url,
          headers,
          cacheControl,
          cacheExpiration,
          send,
          receive,
          cancelToken,
        );
      }

      if (!AppConstant.inProduction) {
        final startTime = DateTime.now();
        response = await executeGenerateRequest();
        final endTime = DateTime.now();
        final duration = endTime.difference(startTime).inMilliseconds;
        Log.d('网络请求耗时 $duration 毫秒,HttpCode:${response.statusCode} HttpMessage:${response.statusMessage} 响应内容 ${response.data}}');
      } else {
        response = await executeGenerateRequest();
      }

      //判断成功与失败, 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;

        //Http处理完了,现在处理 API 的 Code
        if (jsonMap.containsKey('code')) {
          int code = jsonMap['code'];

          // 如果有 code,并且 code = 0 说明成功
          if (code == 0) {
            if (jsonMap['data'] is List<dynamic>) {
              //成功->返回数组
              return HttpResult(
                isSuccess: true,
                code: code,
                msg: jsonMap['msg'],
                listJson: jsonMap['data'], //赋值给的 listJson 字段
              );
            } else {
              //成功->返回对象
              return HttpResult(
                isSuccess: true,
                code: code,
                msg: jsonMap['msg'],
                dataJson: jsonMap['data'], //赋值给的 dataJson 字段
              );
            }

            //如果code !=0 ,下面是错误的情况判断
          } else {
            if (jsonMap.containsKey('errors')) {
              //拿到错误信息对象
              return HttpResult(isSuccess: false, code: code, errorObj: jsonMap['errors'], errorMsg: jsonMap['message']);
            } else if (jsonMap.containsKey('msg')) {
              //如果有msg字符串优先返回msg字符串
              return HttpResult(isSuccess: false, code: code, errorMsg: jsonMap['msg']);
            } else {
              //什么都没有就返回Http的错误字符串
              return HttpResult(isSuccess: false, code: code, errorMsg: jsonMap['message']);
            }
          }
        } else {
          //没有code,说明有错误信息,判断错误信息
          if (jsonMap.containsKey('errors')) {
            //拿到错误信息对象
            return HttpResult(isSuccess: false, errorObj: jsonMap['errors'], errorMsg: jsonMap['message']);
          } else if (jsonMap.containsKey('msg')) {
            //如果有msg字符串优先返回msg字符串
            return HttpResult(isSuccess: false, errorMsg: jsonMap['msg']);
          } else {
            //什么都没有就返回Http的错误字符串
            return HttpResult(isSuccess: false, errorMsg: jsonMap['message']);
          }
        }
      } else {
        //返回Http的错误,给 Http Response 的 statusMessage 值
        return HttpResult(
          isSuccess: false,
          code: response.statusCode ?? ApiConstants.networkDebounceCode,
          errorMsg: response.statusMessage,
        );
      }
    } on DioException catch (e) {
      Log.e("HttpProvider - DioException:$e  其他错误Error:${e.error.toString()}");
      if (e.response != null) {
        // 如果其他的Http网络请求的Code的处理
        Log.d("网络请求错误,data:${e.response?.data}");
        return HttpResult(isSuccess: false, errorMsg: "错误码:${e.response?.statusCode} 错误信息:${e.response?.statusMessage}");
      } else if (e.type == DioExceptionType.connectionTimeout ||
          e.type == DioExceptionType.sendTimeout ||
          e.type == DioExceptionType.receiveTimeout) {
        return HttpResult(isSuccess: false, errorMsg: "网络连接超时,请稍后再试");
      } else if (e.type == DioExceptionType.cancel) {
        return HttpResult(isSuccess: false, errorMsg: "网络请求已取消");
      } else if (e.type == DioExceptionType.badCertificate) {
        return HttpResult(isSuccess: false, errorMsg: "网络连接证书无效");
      } else if (e.type == DioExceptionType.badResponse) {
        return HttpResult(isSuccess: false, errorMsg: "网络响应错误,请稍后再试");
      } else if (e.type == DioExceptionType.connectionError) {
        return HttpResult(isSuccess: false, errorMsg: "网络连接错误,请检查网络连接");
      } else if (e.type == DioExceptionType.unknown) {
        //未知错误中尝试打印具体的错误信息
        if (e.error != null) {
          if (e.error.toString().contains("HandshakeException")) {
            return HttpResult(isSuccess: false, errorMsg: "网络连接错误,请检查网络连接");
          } else {
            return HttpResult(isSuccess: false, errorMsg: e.error.toString()); //这里打印的就是英文错误了,没有格式化
          }
        } else {
          return HttpResult(isSuccess: false, errorMsg: "网络请求出现未知错误");
        }
      } else {
        //如果有response走Api错误
        return HttpResult(isSuccess: false, errorMsg: e.message);
      }
    }
  }

  ///生成对应Get与Post的请求体,并封装对应的参数
  Future<Response> _generateRequest(
    HttpMethod? method,
    Map<String, dynamic>? params,
    Map<String, String>? paths, //文件
    Map<String, Uint8List>? pathStreams, //文件流
    String url,
    Map<String, String>? headers,
    CacheControl? cacheControl,
    Duration? cacheExpiration,
    ProgressCallback? send, // 上传进度监听
    ProgressCallback? receive, // 下载监听
    CancelToken? cancelToken, // 用于取消的 token,可以多个请求绑定一个 token
  ) async {
    if (method != null && method == HttpMethod.POST) {
      //以 Post 请求 FromData 的方式上传
      return networkEngine.executePost(
        url: url,
        params: params,
        paths: paths,
        pathStreams: pathStreams,
        headers: headers,
        send: send,
        receive: receive,
        cancelToken: cancelToken,
      );
    } else {
      //默认 Get 请求,添加逻辑是否需要处理缓存策略,具体缓存逻辑见拦截器

      if (cacheControl != null) {
        if (headers == null || headers.isEmpty) {
          headers = <String, String>{};
        }
        headers['cache_control'] = cacheControl.name;

        if (cacheExpiration != null) {
          headers['cache_expiration'] = cacheExpiration.inMilliseconds.toString();
        }
      }

      return networkEngine.executeGet(
        url: url,
        params: params,
        headers: headers,
        cacheControl: cacheControl,
        cacheExpiration: cacheExpiration,
        receive: receive,
        cancelToken: cancelToken,
      );
    }
  }
}

Http 的工具类我们只需要保持单例并且只留一个入口即可。

五、测试与效果

全部的代码已经就绪,我们测试一下使用的大部分场景。

在 Model 中我们定义一个添加银行卡的方法:

dart 复制代码
 Future<HttpResult<WalletAddEntity>> addBankCard(
    bool isShowLoadingDialog,
    String name,
    String account,
    int bankId,
    String branchName,
    String bankCardImgPath,
  ) async {
    Map<String, dynamic> params = {
      'holder_name': name,
      'number': account,
      'bank_id': bankId,
      'bank_sub_branch': branchName,
    };
    final result = await httpProvider.requestNetResult(
      ApiConstants.apiAddBankCard,
      method: HttpMethod.POST,
      params: params,
      paths: {"image": bankCardImgPath},
      networkDebounce: true,  //启用防抖
      isShowLoadingDialog: isShowLoadingDialog, //是否启动拦截器LoadingDialog
    );
    if (result.isSuccess) {
      final json = result.getDataJson();
      var data = WalletAddEntity.fromJson(json!);
      return result.convert<WalletAddEntity>(data: data);
    } else {
      return result.convert<WalletAddEntity>();
    }
  }

在添加银行卡页面我们调用:

php 复制代码
    final result = await walletRepository.addBankCard(
      true,
      state.name!,
      state.accountTextEditingController.text,
      state.bankEntity!.id!,
      state.branchNameTextEditingController.text,
      state.bankCardFilePath,
    );

    if (result.isSuccess) {
      SmartDialog.showNotify(msg: "银行卡添加成功", notifyType: NotifyType.success);
      Get.back(result: true);
    } else {
      if (result.code != ApiConstants.networkDebounceCode) {
        SmartDialog.showNotify(msg: result.errorMsg ?? '请求失败', notifyType: NotifyType.error);
      }
    }

带文件的Post请求,正常默认带Loading 弹窗:

接下来我们看看异常的情况,我们直接把 Wifi 关掉:

此时真实的 Error 是 HandshakeException ,这里我是做了错误格式化的。

接下来我们连上 Wifi 却把网给断了,此时是有网但是网不通,连接超时。

效果和上面不一样,Loading的时间更长,是 Dio 设置的超时时间。

测试一下防抖效果,由于疯狂点击是同一个请求,所以会丢弃当前的请求等待第一次请求的响应:

Log 如下:

如果类似 CheckBox Switch 这类型的控件, 在快速点击的时候需要防抖的处理,我们也能做到。

Model:

csharp 复制代码
Future<HttpResult> resetRegistrationId(String? registrationId) async {
    Map<String, String> params = {};
    params['registration_id'] = registrationId ?? '';

    //POST请求
    final result = await httpProvider.requestNetResult(
      ApiConstants.apiResetRegistrationId,
      method: HttpMethod.POST,
      params: params,
      networkDebounce: true,  //直接开启去重
    );

    //根据返回的结果,封装原始数据为Bean/Entity对象
    if (result.isSuccess) {
      //重新赋值data或list
      return result.convert();
    }
    return result.convert();
  }

使用:

javascript 复制代码
  void changed(bool checked) {
    _resetRegistrationIdId(checked ? UserService.to.getRegistrationId : "null");
  }

  /// 调用接口重新设置 registrationId
  void _resetRegistrationIdId(String registrationId) async {
    if (UserService.to.isLogin) {
      //获取到数据
      HttpResult result = await mainRepository.resetRegistrationId(registrationId);

      //处理数据
      if (result.isSuccess) {
        Log.d("MainController 重置 registrationId 成功");
      } else {
        Log.d("MainController 重置 registrationId 失败:${result.errorMsg}");
      }
    }
  }

效果 - 手速操作:

Log 如下:

由于每次切换控件的状态,导致每一次请求的参数不同,此时会取消之前的请求,发出新的请求,效果同样也只会执行最后一次。

结语

这就是常用功能的 Dio 封装了,层次比较清晰,欢迎大家试用哦。

要知道其实 Dio 的相关库中很多类似的功能,为什么我要自己的去实现,主要是用过了一些插件并不太好用,由于我们的后端比较特殊,需要自行处理的东西比较多,并且用自己的实现可以更加精准的掌控,才考虑到自己实现了。

你的封装有特殊性吗?如果想用此封装有哪些要改的地方呢?

你大概率需要处理成功的回调,因为大部分后端应该是 Api 错误,如登录接口中返回用户不存在,接口返回的 code 应该是200 成功,而我们后端全部错误都是走的 Error,我们需要在 try catch 中处理错误逻辑。如果你们后端和我们的后端有差异,你需要手动的修改对应的代码。

其次你可能还需要处理错误的格式化文本或国际化之类的,我这里是固定文本,是不推荐的。

拆分功能之后,大家完全就可以选配功能了,想用那种功能添加对应的拦截器即可,如果大家使用中有什么问题可以评论区反馈。

那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出,如果有更多更好更方便的方式也欢迎大家评论区交流。

本文的代码已经全部贴出,部分没贴出的代码可以在前文中找到,也可以到我的 Flutter Demo 查看源码【传送门】

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

Ok,这一期就此完结。

相关推荐
江上清风山间明月23 分钟前
flutter bottomSheet 控件详解
android·flutter·底部导航·bottomsheet
yuanlaile16 小时前
纯Dart Flutter库适配HarmonyOS
flutter·华为·harmonyos·flutter开发鸿蒙·harmonyos教程
yuanlaile16 小时前
Flutter开发HarmonyOS 鸿蒙App的好处、能力以及把Flutter项目打包成鸿蒙应用
flutter·华为·harmonyos·flutter开发鸿蒙
zacksleo17 小时前
鸿蒙原生开发手记:04-一个完整元服务案例
flutter
jcLee952 天前
Flutter/Dart:使用日志模块Logger Easier
flutter·log4j·dart·logger
tmacfrank2 天前
Flutter 异步编程简述
flutter
tmacfrank2 天前
Flutter 基础知识总结
flutter
叫我菜菜就好2 天前
【Flutter_Web】Flutter编译Web第三篇(网络请求篇):dio如何改造方法,变成web之后数据如何处理
前端·网络·flutter
AiFlutter2 天前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
m0_748247803 天前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter