Flutter之Dio拦截器 2024-06-19 周三

2024-06-20  本文已影响0人  松哥888

简介

简单使用,直接封装Dio就可以了,本人的文章Flutter之Dio封装一 2024-06-11 周二就是一次实际练习,确实可行。整个过程下来,感觉Dio相比于AFNetworking和Alamofire甚至是Moya都简单好用,确实不错。
一些听上去比较厉害的功能,都是通过拦截器来实现的,可以随时添加功能,确实是一个很好的设计思路。目前,还出现了配合Dio的一些插件,很多也是通过拦截器实现的,真的很不错。

Interceptor简介

class Interceptor {
  /// The constructor only helps sub-classes to inherit from.
  /// Do not use it directly.
  const Interceptor();

  /// Called when the request is about to be sent.
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    handler.next(options);
  }

  /// Called when the response is about to be resolved.
  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) {
    handler.next(response);
  }

  /// Called when an exception was occurred during the request.
  void onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) {
    handler.next(err);
  }
}
import 'package:dio/dio.dart';
class CustomInterceptors extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    ///super.onRequest(options, handler);
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
    ///super.onResponse(response, handler);
    handler.next(response);
  }

  @override
  Future onError(DioException err, ErrorInterceptorHandler handler) async {
    print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}');
    ///super.onError(err, handler);
    handler.next(err);
  }
}

super.xxx()的写法似是而非,应该用handler.xxx()的形式更直观。除了next之外,还有resolve和reject等方式,类似前端的Promise概念,可以根据实际业务的需要进行选择。

常见的拦截器

1. Token

class TokenInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    options.headers['Authorization'] = LocalStorageUtil.getToken();

    handler.next(options);
  }
}

这里Authorization字段就是Token;一般在登录成功之后会存本地,而这里只要从本地缓存读取就好了,有就设置,没有就给个空字符串。具体的处理,交给后端就可以了。
默认的super.onRequest(options, handler);这里改成了handler.next(options);意图更加明确;

2. Log

选项A:Dio提供deLogInterceptor

  LogInterceptor({
    this.request = true,      /// 这个一般要用
    this.requestHeader = true,   /// 这个很鸡肋,很多信息在request中有;不过一般都要开,前后端有些约定是放在头部的,比如Token之类的
    this.requestBody = false,  /// 这个要改成true,post的参数能显示出来,但是FormData的就不行了
    this.responseHeader = true,  /// 这个要改成false,大多数情况这个不会管
    this.responseBody = false,  /// 这个要改成true,这个是最重要的。响应数据都不看的话,你log个啥,脑子抽风了才会设置为false
    this.error = true,  /// 这个保持true不动
    this.logPrint = _debugPrint,   
  });

/// assert包一下,保证release不输出,比较取巧的做法
void _debugPrint(Object? object) {
  assert(() {
    print(object);
    return true;
  }());
}
/// Note: LogInterceptor should always be the last interceptor added, otherwise modifications by following interceptors will not be logged.
_dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true, responseHeader: false, requestHeader: true));
flutter: *** Request ***
flutter: uri: http://47.92.232.69:8080/sign/signUp/password
flutter: method: POST
flutter: responseType: ResponseType.json
flutter: followRedirects: true
flutter: persistentConnection: true
flutter: connectTimeout: 0:00:05.000000
flutter: sendTimeout: null
flutter: receiveTimeout: 0:00:03.000000
flutter: receiveDataWhenStatusError: true
flutter: extra: {}
flutter: headers:
flutter:  Accept: application/json,*/*
flutter:  ContentType: application/json; charset=utf-8
flutter:  platform: ios
flutter:  content-type: application/json; charset=utf-8
flutter: data:
flutter: {phone: 138xxxxxxxx, password: 123456}
flutter:
flutter: *** Response ***
flutter: uri: http://47.92.232.69:8080/sign/signUp/password
flutter: Response Text:
flutter: {"code":-1,"data":null,"errMsg":"账号不存在"}

选项B:插件pretty_dio_logger

  PrettyDioLogger(
      {this.request = true,       /// 保持true
      this.requestHeader = false,  /// 需要调Header参数的时候改为true,稳定后可以保持false
      this.requestBody = false,   /// 改为true,post参数,表单数据也能看
      this.responseHeader = false,  /// 保持false
      this.responseBody = true,  /// 保持true
      this.error = true, /// 保持默认
      this.maxWidth = 90,  /// 保持默认
      this.compact = true,  /// 保持默认
      this.logPrint = print}); /// 这个要改,系统的print不应该直接用,release模式不应该有log
  /// Debug模式才输出log
  void _debugLogPrint(Object object) {
    if (kDebugMode) {
      print(object);
    }
  }
      _dio.interceptors.add(PrettyDioLogger(
        request: true,
        requestHeader: true,
        requestBody: true,
        responseHeader: false,
        responseBody: true,
        logPrint: _debugLogPrint,
      ));
flutter: ╔╣ Request ║ POST
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Headers
flutter: ╟ Accept: application/json,*/*
flutter: ╟ ContentType: application/json; charset=utf-8
flutter: ╟ platform: ios
flutter: ╟ content-type: application/json; charset=utf-8
flutter: ╟ contentType: application/json; charset=utf-8
flutter: ╟ responseType: ResponseType.json
flutter: ╟ followRedirects: true
flutter: ╟ connectTimeout: 0:00:05.000000
flutter: ╟ receiveTimeout: 0:00:03.000000
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Form data | --dio-boundary-2480454291
flutter: ╟ phone: 15858109291
flutter: ╟ password: 123456
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter:
flutter: ╔╣ Response ║ POST ║ Status: 200
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Body
flutter: ║
flutter: ║    {
flutter: ║         code: 0,
flutter: ║         "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjdXJyZW50IjoxNzE4ODUzNzQzOTkyLCJ1c2VyTmFt
flutter: ║         ZSI6IjE1ODU4MTA5MjkxIiwiZXhwIjoxNzUwMzg5NzQzLCJpYXQiOjE3MTg4NTM3NDN9.M45Ie_Wyr_Hni
flutter: ║         mmtp8-W6NpwH9ueaR-qymKAGXjT9iQ"
flutter: ║         errMsg: "处理成功"
flutter: ║    }
flutter: ║
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔╣ Request ║ POST
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Form data | --dio-boundary-0514650006
flutter: ╟ phone: 15858109291
flutter: ╟ password: 123456
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter:
flutter: ╔╣ Response ║ POST ║ Status: 200
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Body
flutter: ║
flutter: ║    {
flutter: ║         code: 0,
flutter: ║         "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjdXJyZW50IjoxNzE4ODU0NDUyMDgyLCJ1c2VyTmFt
flutter: ║         ZSI6IjE1ODU4MTA5MjkxIiwiZXhwIjoxNzUwMzkwNDUyLCJpYXQiOjE3MTg4NTQ0NTJ9.KkfTtiWFHwjrX
flutter: ║         wOuVcd7p3HQZo0me0eR_mijLbxUcok"
flutter: ║         errMsg: "处理成功"
flutter: ║    }
flutter: ║
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝

通常的做法,一开始会打开Request的Header,和后端对照Header参数;稳定后可以关闭Request的Header,基本上都一样,简洁一些更好。

选项C:自定义Interceptor,自己写一个

这个就不说了,有现成的代码可以抄,根据自己的意愿改。

3. 网络状态检查

class NetworkStatusInterceptor extends Interceptor {
  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    List<ConnectivityResult> resultList =
        await (Connectivity().checkConnectivity());
    if (resultList.contains(ConnectivityResult.none)) {
      LogUtil.log("检测到网络不通,请检查联网状态", level: LogLevel.fatal);
      handler.reject(
          DioException.connectionError(requestOptions: options, reason: "无网络"));
    } else {
      handler.next(options);
    }
  }
}

4. 错误处理

/// The exception enumeration indicates what type of exception
/// has happened during requests.
enum DioExceptionType {
  /// Caused by a connection timeout.
  connectionTimeout,

  /// It occurs when url is sent timeout.
  sendTimeout,

  ///It occurs when receiving timeout.
  receiveTimeout,

  /// Caused by an incorrect certificate as configured by [ValidateCertificate].
  badCertificate,

  /// The [DioException] was caused by an incorrect status code as configured by
  /// [ValidateStatus].
  badResponse,

  /// When the request is cancelled, dio will throw a error with this type.
  cancel,

  /// Caused for example by a `xhr.onError` or SocketExceptions.
  connectionError,

  /// Default error type, Some other [Error]. In this case, you can use the
  /// [DioException.error] if it is not null.
  unknown,
}
{"code":-1,"data":null,"errMsg":"账号不存在"}

有些特殊的业务,需要特殊处理,可以根据和后端的约定,判断code的值,然后在这里统一处理。比如,没有Token的时候,约定code是405,就弹出登录对话框。

class ErrorInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final code = response.data["code"];
    if (code == 405) {
      ToastUtil.showText(text: "Token过期,请重新登录");
    }

    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    switch (err.type) {
      case DioExceptionType.badCertificate:
        LogUtil.log("证书问题", level: LogLevel.warning);
      case DioExceptionType.cancel:
        LogUtil.log("请求被取消了", level: LogLevel.warning);
      case DioExceptionType.connectionError:
        LogUtil.log("连接问题,请检查网络状态", level: LogLevel.warning);
      default:
      // 什么也不做
    }

    handler.next(err);
  }
}

拦截器统一管理

将添加拦截器的代码集中到一个文件,简化主流程的复杂度。

class InterceptorManager {
  /// 单例
  static final InterceptorManager _instance = InterceptorManager._internal();

  factory InterceptorManager() => _instance;

  InterceptorManager._internal() {
    /// 网络状态拦截器开始侦听网络
    networkStatusInterceptor = NetworkStatusInterceptor();
    networkStatusInterceptor.startMonitorNetworkStatus();
  }

  /// 网络状态拦截器
  late NetworkStatusInterceptor networkStatusInterceptor;

  /// iOS模拟器,插件connectivity_plus有问题,所以给这个拦截器加个开关
  bool isCheckNetworkStatus = false;

  /// 添加拦截器
  void addTo(Dio dio) {
    /// 网络状态, 一般放第一个
    if (isCheckNetworkStatus) {
      dio.interceptors.add(networkStatusInterceptor);
    }

    /// token
    dio.interceptors.add(TokenInterceptor());

    /// 错误处理
    dio.interceptors.add(ErrorInterceptor());

    /// 日志; 一般放最后
    dio.interceptors.add(PrettyDioLogger(
      request: true,
      requestHeader: false,
      requestBody: true,
      responseHeader: false,
      responseBody: true,
      logPrint: _debugLogPrint,
    ));
  }

  /// Debug模式才输出log
  void _debugLogPrint(Object object) {
    if (kDebugMode) {
      print(object);
    }
  }
}

Loading和Toast

这两个没什么内容,并且需要变量控制,还是直接放在主流程比较好,不适合做成拦截器。考虑了Loading和Toast之后的request代码变成如下的样子:

  Future<BaseResponse> doRequest(
    String path,
    HttpMethod method, {
    Map<String, dynamic>? parameters,
    bool isShowLoading = false,
    bool isShowToast = true,
  }) async {
    Options options = Options(method: method.value);

    /// 根据method类型处理参数
    Object? data;
    Map<String, dynamic>? queryParameters;
    switch (method) {
      case HttpMethod.get:
        queryParameters = parameters;
      case HttpMethod.post:
        data = parameters;
      case HttpMethod.postFormData:
        if (parameters != null) {
          data = FormData.fromMap(parameters);
        }
    }

    /// 包装成自定义的响应;后端自定义的数据优先
    BaseResponse baseResponse = BaseResponse();
    try {
      if (isShowLoading) {
        LottieUtils.showToastLoading();
      }
      /// 统一调用request进行数据传输
      final dioResponse = await _dio.request(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
      );
      baseResponse.code = dioResponse.data?["code"];
      baseResponse.data = dioResponse.data?["data"];
      baseResponse.errMsg = dioResponse.data?["errMsg"];
    } on DioException catch (e) {
      /// 异常时,将DioException转化为自定义的baseResponse
      baseResponse = BaseResponse.fromException(e);
    } finally {
      if (isShowLoading) {
        LottieUtils.hideToastLoading();
      }
      if (isShowToast && !baseResponse.isSuccess) {
        ToastUtil.showText(text: baseResponse.errMsg ?? "");
      }
    }
    return baseResponse;
  }

缓存和Cookie

这两个有现成的插件可用,暂时也不知道具体的价值,也没有想到必须导入的原因,网上的评价也一般,所以不考虑引入,等需要的时候再加就可以了。拦截器可以随时加,很方便的。另外,重试,代理等功能也是一样,不是强需求,可以必要的时候再考虑加。
dio_cache_interceptor
dio_cookie_manager

上一篇 下一篇

猜你喜欢

热点阅读