5

Flutter Dio 亲妈级别封装教程

 2 years ago
source link: https://segmentfault.com/a/1190000040831841
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

为什么一定要封装一手?

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

Cookie???滚犊子

不管cookie,再见

全局初始化,传入参数

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

dio初始化的配置

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

如果要白嫖完整的方案

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

未标题-2.jpg

未标题-3.jpg

项目地址 github地址
百度云 提取码:xi6b
蓝奏云 提取码:7agj

这里说下拦截器,可以在初始化的时候传入,也可以手写传入,例如我这里定义了四个拦截器,第一个用于全局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);
    }
}

未标题-4.jpg

这里的提示就是自定义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);
    }
  }

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK