Flutter Dio 亲妈级别封装教程

共 47622字,需浏览 96分钟

 ·

2021-10-22 15:20

作者:油糕

来源:SegmentFault 思否社区


前不久看到 艾维码 大佬的dio封装,经过摸索,改吧改吧,使用的不错。对于之前 艾维码 大佬文章中一些已经失效的做了修正

为什么一定要封装一手?

token拦截,错误拦截,统一错误处理,统一缓存,信息封装(错误,正确)

Cookie???滚犊子

不管cookie,再见

全局初始化,传入参数

dio初始化,传入baseUrl, connectTimeout, receiveTimeout,options,header 拦截器等。dio初始化的时候允许我们传入的一些配置

dio初始化的配置

这里说下,之前 艾维码 大佬的帖子中的options,最新版的dio已经使用requestOptions, 之前的merge,现在使用copyWith。详情向下看

如果要白嫖完整的方案

可以参考使用这套方案开发的 flutter + getx 仿开眼视频app,有star的大佬可以赏点star。



项目地址 https://github.com/abcd498936590/flutter_eyepetizer

apk下载 https://pan.baidu.com/share/init?surl=5T13BzbwG_NI8pbaVISoDA

 提取码:3ev2

初始化

这里说下拦截器,可以在初始化的时候传入,也可以手写传入,例如我这里定义了四个拦截器,第一个用于全局request时候给请求投加上context-type:json。第二个是全局错误处理拦截器,下面的内容会介绍拦截器部分。
cache拦截器,全局处理接口缓存数据,retry重试拦截器(我暂时没怎么用)

·class Http {
      static final Http _instance = Http._internal();
      // 单例模式使用Http类,
      factory Http() => _instance;

      static late final Dio dio;
      CancelToken _cancelToken = new CancelToken();

      Http._internal() {
        // BaseOptions、Options、RequestOptions 都可以配置参数,优先级别依次递增,且可以根据优先级别覆盖参数
        BaseOptions options = new BaseOptions();

        dio = Dio(options);

        // 添加request拦截器
        dio.interceptors.add(RequestInterceptor());
        // 添加error拦截器
        dio.interceptors.add(ErrorInterceptor());
        // // 添加cache拦截器
        dio.interceptors.add(NetCacheInterceptor());
        // // 添加retry拦截器
        dio.interceptors.add(
          RetryOnConnectionChangeInterceptor(
            requestRetrier: DioConnectivityRequestRetrier(
              dio: dio,
              connectivity: Connectivity(),
            ),
          ),
        );

    // 在调试模式下需要抓包调试,所以我们使用代理,并禁用HTTPS证书校验
    // if (PROXY_ENABLE) {
    //   (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
    //       (client) {
    //     client.findProxy = (uri) {
    //       return "PROXY $PROXY_IP:$PROXY_PORT";
    //     };
    //     //代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
    //     client.badCertificateCallback =
    //         (X509Certificate cert, String host, int port) => true;
    //   };
    // }
      }

      ///初始化公共属性
      ///
      /// [baseUrl] 地址前缀
      /// [connectTimeout] 连接超时赶时间
      /// [receiveTimeout] 接收超时赶时间
      /// [interceptors] 基础拦截器
      void init({
        String? baseUrl,
        int connectTimeout = 1500,
        int receiveTimeout = 1500,
        Map<String, String>? headers,
        List<Interceptor>? interceptors,
      }) {
        dio.options = dio.options.copyWith(
          baseUrl: baseUrl,
          connectTimeout: connectTimeout,
          receiveTimeout: receiveTimeout,
          headers: headers ?? const {},
        );
        // 在初始化http类的时候,可以传入拦截器
        if (interceptors != null && interceptors.isNotEmpty) {
          dio.interceptors..addAll(interceptors);
        }
      }

      // 关闭dio
      void cancelRequests({required CancelToken token}) {
        _cancelToken.cancel("cancelled");
      }

      // 添加认证
      // 读取本地配置
      Map<String, dynamic>? getAuthorizationHeader() {
        Map<String, dynamic>? headers;
        // 从getx或者sputils中获取
        // String accessToken = Global.accessToken;
        String accessToken = "";
        if (accessToken != null) {
          headers = {
            'Authorization''Bearer $accessToken',
          };
        }
        return headers;
      }

      Future get(
        String path, {
        Map<String, dynamic>? params,
        Options? options,
        CancelToken? cancelToken,
        bool refresh = false,
        bool noCache = !CACHE_ENABLE,
        String? cacheKey,
        bool cacheDisk = false,
      }) async {
        Options requestOptions = options ?? Options();
        requestOptions = requestOptions.copyWith(
          extra: {
            "refresh": refresh,
            "noCache": noCache,
            "cacheKey": cacheKey,
            "cacheDisk": cacheDisk,
          },
        );
        Map<String, dynamic>? _authorization = getAuthorizationHeader();
        if (_authorization != null) {
          requestOptions = requestOptions.copyWith(headers: _authorization);
        }
        Response response;
        response = await dio.get(
          path,
          queryParameters: params,
          options: requestOptions,
          cancelToken: cancelToken ?? _cancelToken,
        );

        return response.data;
      }

      Future post(
        String path, {
        Map<String, dynamic>? params,
        data,
        Options? options,
        CancelToken? cancelToken,
      }) async {
        Options requestOptions = options ?? Options();
        Map<String, dynamic>? _authorization = getAuthorizationHeader();
        if (_authorization != null) {
          requestOptions = requestOptions.copyWith(headers: _authorization);
        }
        var response = await dio.post(
          path,
          data: data,
          queryParameters: params,
          options: requestOptions,
          cancelToken: cancelToken ?? _cancelToken,
        );
        return response.data;
      }

      Future put(
        String path, {
        data,
        Map<String, dynamic>? params,
        Options? options,
        CancelToken? cancelToken,
      }) async {
        Options requestOptions = options ?? Options();

        Map<String, dynamic>? _authorization = getAuthorizationHeader();
        if (_authorization != null) {
          requestOptions = requestOptions.copyWith(headers: _authorization);
        }
        var response = await dio.put(
          path,
          data: data,
          queryParameters: params,
          options: requestOptions,
          cancelToken: cancelToken ?? _cancelToken,
        );
        return response.data;
      }

      Future patch(
        String path, {
        data,
        Map<String, dynamic>? params,
        Options? options,
        CancelToken? cancelToken,
      }) async {
        Options requestOptions = options ?? Options();
        Map<String, dynamic>? _authorization = getAuthorizationHeader();
        if (_authorization != null) {
          requestOptions = requestOptions.copyWith(headers: _authorization);
        }
        var response = await dio.patch(
          path,
          data: data,
          queryParameters: params,
          options: requestOptions,
          cancelToken: cancelToken ?? _cancelToken,
        );
        return response.data;
      }

      Future delete(
        String path, {
        data,
        Map<String, dynamic>? params,
        Options? options,
        CancelToken? cancelToken,
      }) async {
        Options requestOptions = options ?? Options();

        Map<String, dynamic>? _authorization = getAuthorizationHeader();
        if (_authorization != null) {
          requestOptions = requestOptions.copyWith(headers: _authorization);
        }
        var response = await dio.delete(
          path,
          data: data,
          queryParameters: params,
          options: requestOptions,
          cancelToken: cancelToken ?? _cancelToken,
        );
        return response.data;
      }
    }

dio拦截器

下面我们来看下拦截器,下面是一个处理处理拦截器案例

// 这里是一个我单独写得soket错误实例,因为dio默认生成的是不允许修改message内容的,我只能自定义一个使用
    class MyDioSocketException extends SocketException {
      late String message;

      MyDioSocketException(
        message, {
        osError,
        address,
        port,
      }) : super(
              message,
              osError: osError,
              address: address,
              port: port,
            );
    }

    /// 错误处理拦截器
    class ErrorInterceptor extends Interceptor {
      // 是否有网
      Future<bool> isConnected() async {
        var connectivityResult = await (Connectivity().checkConnectivity());
        return connectivityResult != ConnectivityResult.none;
      }

      @override
      Future<void> onError(DioError err, ErrorInterceptorHandler errCb) async {
        // 自定义一个socket实例,因为dio原生的实例,message属于是只读的
        // 这里是我单独加的,因为默认的dio err实例,的几种类型,缺少无网络情况下的错误提示信息
        // 这里我手动做处理,来加工一手,效果,看下面的图片,你就知道
        if (err.error is SocketException) {
          err.error = MyDioSocketException(
            err.message,
            osError: err.error?.osError,
            address: err.error?.address,
            port: err.error?.port,
          );
        }
        // dio默认的错误实例,如果是没有网络,只能得到一个未知错误,无法精准的得知是否是无网络的情况
        if (err.type == DioErrorType.other) {
          bool isConnectNetWork = await isConnected();
          if (!isConnectNetWork && err.error is MyDioSocketException) {
            err.error.message = "当前网络不可用,请检查您的网络";
          }
        }
        // error统一处理
        AppException appException = AppException.create(err);
        // 错误提示
        debugPrint('DioError===: ${appException.toString()}');
        err.error = appException;
        return super.onError(err, errCb);
      }
    }


以上的代码可以看到,ErrorInterceptor类继承自Interceptor,可以重新onRequest 、onResponse、onError,三个状态,最后return super.onError将err实例传递给超类。

统一的错误信息包装处理

试想一下,如果你的项目,有十几种状态码,每种也也都需要吧code码转换成文字信息,因为有时候你需要给用户提示。例如:连接超时,请求失败,网络错误,等等。下面是统一的错误处理

AppException.dart

import 'package:dio/dio.dart';

/// 自定义异常
class AppException implements Exception {
  final String _message;
  final int _code;

  AppException(
    this._code,
    this._message,
  );

  String toString() {
    return "$_code$_message";
  }

  String getMessage() {
    return _message;
  }

  factory AppException.create(DioError error) {
    switch (error.type) {
      case DioErrorType.cancel:
        {
          return BadRequestException(-1, "请求取消");
        }
      case DioErrorType.connectTimeout:
        {
          return BadRequestException(-1, "连接超时");
        }
      case DioErrorType.sendTimeout:
        {
          return BadRequestException(-1, "请求超时");
        }
      case DioErrorType.receiveTimeout:
        {
          return BadRequestException(-1, "响应超时");
        }
      case DioErrorType.response:
        {
          try {
            int? errCode = error.response!.statusCode;
            // String errMsg = error.response.statusMessage;
            // return ErrorEntity(code: errCode, message: errMsg);
            switch (errCode) {
              case 400:
                {
                  return BadRequestException(errCode!, "请求语法错误");
                }
              case 401:
                {
                  return UnauthorisedException(errCode!, "没有权限");
                }
              case 403:
                {
                  return UnauthorisedException(errCode!, "服务器拒绝执行");
                }
              case 404:
                {
                  return UnauthorisedException(errCode!, "无法连接服务器");
                }
              case 405:
                {
                  return UnauthorisedException(errCode!, "请求方法被禁止");
                }
              case 500:
                {
                  return UnauthorisedException(errCode!, "服务器内部错误");
                }
              case 502:
                {
                  return UnauthorisedException(errCode!, "无效的请求");
                }
              case 503:
                {
                  return UnauthorisedException(errCode!, "服务器挂了");
                }
              case 505:
                {
                  return UnauthorisedException(errCode!, "不支持HTTP协议请求");
                }
              default:
                {
                  // return ErrorEntity(code: errCode, message: "未知错误");
                  return AppException(errCode!, error.response!.statusMessage!);
                }
            }
          } on Exception catch (_) {
            return AppException(-1, "未知错误");
          }
        }
      default:
        {
          return AppException(-1, error.error.message);
        }
    }
  }
}

/// 请求错误
class BadRequestException extends AppException {
  BadRequestException(int code, String message) : super(code, message);
}

/// 未认证异常
class UnauthorisedException extends AppException {
  UnauthorisedException(int code, String message) : super(code, message);
}


使用的时候这样使用,

Future<ApiResponse<Feed>> getFeedData(url) async {
    try {
      dynamic response = await HttpUtils.get(url);
      // print(response);
      Feed data = Feed.fromJson(response);
      return ApiResponse.completed(data);
    } on DioError catch (e) {
      print(e);
      // 这里看这里,如果是有错误的请求下,使用AppException对错误对象进行处理
      // 处理过后,你就可以比如弹个toast,提示给用户等,
      // 弹窗toast等在下面的方法中调用
      return ApiResponse.error(e.error);
    }
  }
  
Future<void> _refresh() async {
    
    ApiResponse<Feed> swiperResponse = await getFeedData(initPageUrl);
    
    // 加工过后,我们可以获得两个状态,Status.COMPLETED 和 Status.ERROR
    // 看这里
    if (swiperResponse.status == Status.COMPLETED) {
        // 成功的代码,想干嘛干嘛
    }else if (swiperResponse.status == Status.ERROR) {
        // 失败的代码,可以给个toast,提示给用户
        // 例如我在这里提示用户
        // 使用 exception!.getMessage(); 获得错误对象的文字信息,是我们拦截器处理过后的提示文字,非英文,拿到这,提示给用户不香吗???看下面的图片效果
        String errMsg = swiperResponse.exception!.getMessage();
        publicToast(errMsg);
    }
}



这里的提示就是自定义err拦截器中增加的代码,对于dio不能够得到是否无网络的补充

磁盘缓存数据,拦截器

磁盘缓存接口数据,首先我们要封装一个SpUtil类,
sputils.dart

class SpUtil {
  SpUtil._internal();
  static final SpUtil _instance = SpUtil._internal();

  factory SpUtil() {
    return _instance;
  }

  SharedPreferences? prefs;

  Future<void> init() async {
    prefs = await SharedPreferences.getInstance();
  }

  Future<bool> setJSON(String key, dynamic jsonVal) {
    String jsonString = jsonEncode(jsonVal);
    return prefs!.setString(key, jsonString);
  }

  dynamic getJSON(String key) {
    String? jsonString = prefs?.getString(key);
    return jsonString == null ? null : jsonDecode(jsonString);
  }

  Future<bool> setBool(String key, bool val) {
    return prefs!.setBool(key, val);
  }

  bool? getBool(String key) {
    return prefs!.getBool(key);
  }

  Future<bool> remove(String key) {
    return prefs!.remove(key);
  }
}

缓存拦截器

const int CACHE_MAXAGE = 86400000;
const int CACHE_MAXCOUNT = 1000;
const bool CACHE_ENABLE = false;

class CacheObject {
  CacheObject(this.response)
      : timeStamp = DateTime.now().millisecondsSinceEpoch;
  Response response;
  int timeStamp;

  @override
  bool operator ==(other) {
    return response.hashCode == other.hashCode;
  }

  @override
  int get hashCode => response.realUri.hashCode;
}

class NetCacheInterceptor extends Interceptor {
  // 为确保迭代器顺序和对象插入时间一致顺序一致,我们使用LinkedHashMap
  var cache = LinkedHashMap<String, CacheObject>();

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler requestCb,
  ) async {
    if (!CACHE_ENABLE) {
      return super.onRequest(options, requestCb);
    }

    // refresh标记是否是刷新缓存
    bool refresh = options.extra["refresh"] == true;

    // 是否磁盘缓存
    bool cacheDisk = options.extra["cacheDisk"] == true;

    // 如果刷新,先删除相关缓存
    if (refresh) {
      // 删除uri相同的内存缓存
      delete(options.uri.toString());

      // 删除磁盘缓存
      if (cacheDisk) {
        await SpUtil().remove(options.uri.toString());
      }

      return;
    }

    // get 请求,开启缓存
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == 'get') {
      String key = options.extra["cacheKey"] ?? options.uri.toString();

      // 策略 1 内存缓存优先,2 然后才是磁盘缓存

      // 1 内存缓存
      var ob = cache[key];
      if (ob != null) {
        //若缓存未过期,则返回缓存内容
        if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
            CACHE_MAXAGE) {
          return;
        } else {
          //若已过期则删除缓存,继续向服务器请求
          cache.remove(key);
        }
      }

      // 2 磁盘缓存
      if (cacheDisk) {
        var cacheData = SpUtil().getJSON(key);
        if (cacheData != null) {
          return;
        }
      }
    }
    return super.onRequest(options, requestCb);
  }

  @override
  void onResponse(
      Response response, ResponseInterceptorHandler responseCb) async {
    // 如果启用缓存,将返回结果保存到缓存
    if (CACHE_ENABLE) {
      await _saveCache(response);
    }
    return super.onResponse(response, responseCb);
  }

  Future<void> _saveCache(Response object) async {
    RequestOptions options = object.requestOptions;

    // 只缓存 get 的请求
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == "get") {
      // 策略:内存、磁盘都写缓存

      // 缓存key
      String key = options.extra["cacheKey"] ?? options.uri.toString();

      // 磁盘缓存
      if (options.extra["cacheDisk"] == true) {
        await SpUtil().setJSON(key, object.data);
      }

      // 内存缓存
      // 如果缓存数量超过最大数量限制,则先移除最早的一条记录
      if (cache.length == CACHE_MAXCOUNT) {
        cache.remove(cache[cache.keys.first]);
      }

      cache[key] = CacheObject(object);
    }
  }

  void delete(String key) {
    cache.remove(key);
  }
}

开始封装

class HttpUtils {
  static void init({
    required String baseUrl,
    int connectTimeout = 1500,
    int receiveTimeout = 1500,
    List<Interceptor>? interceptors,
  }) {
    Http().init(
      baseUrl: baseUrl,
      connectTimeout: connectTimeout,
      receiveTimeout: receiveTimeout,
      interceptors: interceptors,
    );
  }

  static void cancelRequests({required CancelToken token}) {
    Http().cancelRequests(token: token);
  }

  static Future get(
    String path, {
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
    bool refresh = false,
    bool noCache = !CACHE_ENABLE,
    String? cacheKey,
    bool cacheDisk = false,
  }) async {
    return await Http().get(
      path,
      params: params,
      options: options,
      cancelToken: cancelToken,
      refresh: refresh,
      noCache: noCache,
      cacheKey: cacheKey,
    );
  }

  static Future post(
    String path, {
    data,
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    return await Http().post(
      path,
      data: data,
      params: params,
      options: options,
      cancelToken: cancelToken,
    );
  }

  static Future put(
    String path, {
    data,
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    return await Http().put(
      path,
      data: data,
      params: params,
      options: options,
      cancelToken: cancelToken,
    );
  }

  static Future patch(
    String path, {
    data,
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    return await Http().patch(
      path,
      data: data,
      params: params,
      options: options,
      cancelToken: cancelToken,
    );
  }

  static Future delete(
    String path, {
    data,
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    return await Http().delete(
      path,
      data: data,
      params: params,
      options: options,
      cancelToken: cancelToken,
    );
  }
}

注入,初始化

main。dart。这里参考我个人的使用例子

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // debugPaintSizeEnabled = true;
  await initStore();
  runApp(MyApp());
}

Future<void> initStore() async {
  // 初始化本地存储类
  await SpUtil().init();
  // 初始化request类
  HttpUtils.init(
    baseUrl: Api.baseUrl,
  );
  // 历史记录,全局 getx全局注入,
  await Get.putAsync(() => HistoryService().init());
  print("全局注入");
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      debugShowCheckedModeBanner: false,
      initialRoute: PageRoutes.INIT_ROUTER,
      getPages: PageRoutes.routes,
    );
  }
}

使用封装好得例子
// 这里定义一个函数,返回的是future 《apiResponse》,可以得到status的状态
Future<ApiResponse<Feed>> getFeedData(url) async {
    try {
      dynamic response = await HttpUtils.get(url);
      // print(response);
      Feed data = Feed.fromJson(response);
      return ApiResponse.completed(data);
    } on DioError catch (e) {
      print(e);
      return ApiResponse.error(e.error);
    }
  }

  Future<void> _refresh() async {
    
    ApiResponse<Feed> swiperResponse = await getFeedData(initPageUrl);
    if (!mounted) {
      return;
    }
    // 使用 status.COMPLETED 判断是否成功
    if (swiperResponse.status == Status.COMPLETED) {
      setState(() {
        nextPageUrl = swiperResponse.data!.nextPageUrl;
        _swiperList = [];
        _swiperList.addAll(swiperResponse.data!.issueList![0]!.itemList!);
        _itemList = [];
      });
      // 拉取新的,列表
      await _loading();
      // 使用 status.ERROR 判断是否失败
    } else if (swiperResponse.status == Status.ERROR) {
      setState(() {
        stateCode = 2;
      });
      // 错误的话,我们可以调用 getMessage() 获取错误信息。提示给用户(汉化后的友好提示语)
      String errMsg = swiperResponse.exception!.getMessage();
      publicToast(errMsg);
      print("发生错误,位置home bottomBar1 swiper, url: ${initPageUrl}");
      print(swiperResponse.exception);
    }
  }

如果要白嫖完整的方案

可以参考使用这套方案开发的 flutter + getx 仿开眼视频app,有star的大佬可以赏点star。

项目地址 https://github.com/abcd498936590/flutter_eyepetizer

apk下载 https://pan.baidu.com/share/init?surl=5T13BzbwG_NI8pbaVISoDA

 提取码:3ev2



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -


浏览 74
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报