Flutter 学习 之 DIO4.0 的封装

dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...
网址在右边 → dio

一.引入插件

在 pubspec.yaml 文件下新增 dio(注意空格问题)

dependencies:
  dio: ^4.0.6

二. 封装DIO

1.创建DioClient单例模式,实现访问方法

// 必须是顶层函数
_parseAndDecode(String response) {
  return jsonDecode(response);
}

parseJson(String text) {
  return compute(_parseAndDecode, text);
}
  //继承DioForNavigator(详情见官方文档) 
class DioClient extends DioForNative {
  //单例模式
  static DioClient? _instance;
  factory DioClient() => _instance ??= DioClient._init();
    //初始化方法
  DioClient._init() {
    (transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
    options = BaseOptions(
    //设定一些基础的东西
      connectTimeout: 60*1000,//连接超时间
      receiveTimeout: 60*1000,//接收超时时间
      //除了在这里定义还可以到拦截器中定义
    );
    //处理访问前的拦截器
    interceptors.add(OptionInterceptor());
    //处理回来的数据
    interceptors.add(RequestInterceptor());
    //代理抓包(开发阶段可能用到,正式上线建议关闭)
    proxy();
  }

  ///get请求
  doGet<T>(path, {queryParameters, options, cancelToken, onReceiveProgress}) {
    return get<T>(path,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onReceiveProgress: onReceiveProgress);
  }

  ///post请求 为了不和继承的DioMixin里面的post方法名冲突所以起名叫doPost
  doPost<T>(path,
      {queryParameters,
      options,
      cancelToken,
      onSendProgress,
      onReceiveProgress}) {
    return post<T>(path,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress);
  }
  ///上传文件
  uploadFile(formData) {
    var uploadOptions = Options(contentType: "multipart/form-data");
    return doPost(Api.uploadURL, options: uploadOptions, data: formData);
  }
  ///代理抓包测试用
  void proxy() {
    if (NetworkConfig.proxyEnable) {
      if (NetworkConfig.caughtAddress != "") {
        (httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
            (client) {
          client.findProxy = (Uri uri) {
            return 'PROXY ' + NetworkConfig.caughtAddress;
          };
          client.badCertificateCallback =
              (X509Certificate cert, String host, int port) {
            return true;
          };
        };
      }
    }
  }
}

2.封装拦截器

dio的请求流程是 请求拦截器 >> 请求转换器 >> 发起请求 >> 响应转换器 >> 响应拦截器 >> 最终结果。

请求拦截器

//Option拦截器可以用来统一处理Option信息 可以在这里添加
class OptionInterceptor extends InterceptorsWrapper {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    //在请求发起前修改头部
    // options.headers["token"] = "11111";
  ///请求的Content-Type,默认值是"application/json; charset=utf-8".
  /// 如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
    options.contentType=Headers.formUrlEncodedContentType;
///如果你的headers是固定的你可以在BaseOption中设置,如果不固定可以在这里进行根据条件设置
    options.headers["apiToken"] = "111222154546";
    options.headers["user-token"]=CacheUtil().getJson(SPName.userInfo)!["userToken"];
    String? mainUrl = CacheUtil().get<String>(SPName.mainUrl);
    //修改地址
    //如果需要改变主地址可以在这里设置
    if (StringUtil.isNotEmpty(mainUrl)) {
      options.baseUrl = mainUrl!;
    } else {
      options.baseUrl = NetworkConfig.baseUrl;
    }
    //开发阶段可以把地址带参数打印出来方便校验结果
    debugPrint(
        "request:${options.method}\t url-->${options.baseUrl}${options.path}?${FormatUtil.formattedUrl(options.queryParameters)}");

    if (options.queryParameters["hideLoading"] != true) {
      EasyLoading.show();
    }
// 一定要加上这句话 否则进入不了下一步
    return handler.next(options);
  }
}

  ///格式化url,将post和get请求以get链接输出
  static String formattedUrl(params) {
    var urlParamsStr = "";
    if (params?.isNotEmpty ?? false) {
      var tempArr = [];
      params.forEach((k, v) {
        tempArr.add(k + '=' + v.toString());
      });
      urlParamsStr = tempArr.join('&');
    }
    return urlParamsStr;
  }

响应拦截器

这一部分需要和实际相结合,根据每个后端返回的数据不同灵活配置

///拦截器 数据初步处理
class RequestInterceptor extends InterceptorsWrapper {
  //请求后 成功走这里
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    EasyLoading.dismiss();
    if (response.statusCode == 200) {
      //访问正确有返回值的情况
      if (response.data is Map) {
        //将数据脱壳需要返回自己的数据
        ResponseData responseData = ResponseData.fromJson(response.data);
        if (responseData.success) {
          response.data = responseData.data;
          response.statusCode = responseData.respCode;
          response.statusMessage = responseData.respDesc;
          return handler.resolve(response);
        }
        return handler.resolve(response);
      } else if (response.data is String) {
        //  {"respCode":403,"respDesc":"非法访问"}
        ResponseError model = ResponseError.fromJson(jsonDecode(response.data));
        response.statusCode = model.respCode;
        if (model.respCode == 403 || model.respCode == 402) {
          //做些什么
          throwUnauthorizedError(response);
        }else{
          throwError(response);
        }
      } else {
        throwError(response);
      }
    } else {
      throwError(response);
    }
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    EasyLoading.dismiss();

    throw DioError(
        requestOptions: err.requestOptions,
        type: err.type,
        error: err,
        response: err.response);
  }

  ///抛出异常 留给baseModel去统一处理
  void throwError(Response<dynamic> response) {
    throw DioError(
        requestOptions: response.requestOptions,
        error: ResponseException(errCode: response.statusCode));
  }
}
  ///鉴权错误
  void throwUnauthorizedError(Response<dynamic> response) {
    throw DioError(
        requestOptions: response.requestOptions,
        error: UnauthorizedError(errCode: response.statusCode));
  }

上述中用到的类

abstract class BaseResponseData{
  int? respCode;
  String? respDesc;
  dynamic attribute;
  dynamic data;
  bool get success;

  BaseResponseData({this.respCode, this.respDesc, this.attribute, this.data});

  @override
  String toString() {
    return 'BaseRespData{code: $respCode, message: $respDesc, data: $attribute}';
  }
}

class ResponseData extends BaseResponseData {
  @override
  bool get success => respCode != null || data != null;

  ResponseData.fromJson(Map<String, dynamic> json) {
    if (json['respCode'] != null && json['respCode'] is String) {
      json['respCode'] = int.parse(json['respCode']);
    }
    respCode = json['respCode'] ?? json['code'];
    respDesc = json['respDesc'] ?? json['message'] ?? json['msg'];
    attribute = json['attribute'] ?? json["data"];
    if (attribute != null) {
      if (attribute is Map && attribute.containsKey("data")) {
        data = attribute['data'];
      } else {
        data = attribute;
      }
    } else {
      data = json;
    }
  }
}


class ResponseError extends BaseResponseData {

  ResponseError.fromJson(Map<String, dynamic> json) {
    respDesc = json["respDesc"];
    respCode = json["respCode"];
  }

  Map<String, dynamic> toJson() {
    Map<String, dynamic> data = {};
    data["respDesc"] = respDesc;
    data["respCode"] = respCode;
    return data;
  }

  @override
  // TODO: implement success
  bool get success => false;
}

class ResponseException implements Exception {
  int? errCode;
  String? errMsg;

  ResponseException({this.errCode});

  int? get errorCode => errCode;

//statusCode==200时候返回的data中存在的respCode
  String? get errorMessage {
    String msg = errMsg ?? "";
    switch (errCode) {
      default:
    }
    return msg;
  }
  @override
  String toString() {
    return 'RequestException{errorCode: $errorCode, errorMessage: $errorMessage}';
  }
}

捕获错误并提示

DioErrorType 分六种 connectTimeout,sendTimeout,receiveTimeout,response,cancel,other,
其实加上刚才我们自定义type 总共可以分成四类 超时的 返回错误的 取消的 和其他

  ///格式化Dio返回的Error
  ///[e] catch到的error
  static ErrorMessageModel dioErrorFormat(e) {
    int? errorCode;
    StateErrorType errorType = StateErrorType.defaultError;
    String errMsg = "网络离家出走了~";
    //判断一下抛出的异常是不是DIO包裹的异常
    if (e is DioError) {
      //是不是各种超时
      if (e.type == DioErrorType.receiveTimeout ||
          e.type == DioErrorType.sendTimeout ||
          e.type == DioErrorType.receiveTimeout) {
        errorType = StateErrorType.networkTimeoutError;
        errMsg = "连接超时了";
      } else if (e.type == DioErrorType.response) {
        //访问出的各种错 访问中statusCode是400/500代码都会走到这里 如果想详细展示具体是什么错误可以继续细分
        errorType = StateErrorType.responseException;
        errMsg = _getNumberMeans(e);
      } else if (e.type == DioErrorType.cancel) {
        //如果是取消访问了走这里
        errMsg = e.message;
      } else {
        //这里是刚才DIOerror包裹的自定义错误
       // 这里由于没有定义error.type所以用error的类型判断
        dynamic otherError = e.error;
        dynamic otherE;
        if (otherError is DioError) {
          otherE = otherError.error;
        }
        if (otherE is ResponseException) {
          errorType = StateErrorType.responseException;
          errMsg = otherE.errorMessage ?? "";
          errorCode = otherE.errorCode;
        } else if (otherE is SocketException) {
          errorType = StateErrorType.networkTimeoutError;
          errMsg = "网络无连接,请检查网络设置";
        } else {
          errorType = StateErrorType.defaultError;
          errMsg = "网络无连接,请检查网络设置";
        }
      }
    } else {
      errorType = StateErrorType.defaultError;
      errMsg = "出问题了~~~";
    }
    return ErrorMessageModel(
        errorType: errorType, message: errMsg, errorCode: errorCode);
  }

将获取到的状态码转成中文提示

  ///获取到的数值转换成文字
  static String _getNumberMeans(DioError e) {
    String str;
    if (e.response?.statusCode != null) {
      switch (e.response?.statusCode) {
        case 400:
          str = "[${e.response?.statusCode}] 参数有误";
          break;
        case 402:
          str = "[${e.response?.statusCode}] 啊 这是一个非法请求呢";
          break;
        case 403:
          str = "[${e.response?.statusCode}] 服务器拒绝请求";
          break;
        case 404:
          str = "[${e.response?.statusCode}] 访问地址不存在";
          break;
        case 405:
          str = "[${e.response?.statusCode}] 请求方式错误";
          break;
        case 500:
          str = "[${e.response?.statusCode}] 服务器内部出错了";
          break;
        case 502:
          str = "[${e.response?.statusCode}] 无效的请求哦";
          break;
        case 503:
          str = "[${e.response?.statusCode}] 服务器说他在忙";
          break;
        case 505:
          str = "[${e.response?.statusCode}] 不支持的HTTP协议";
          break;
        default:
          str = "[${e.response?.statusCode}] 未知错误";
          break;
      }
    } else {
      str = e.message;
    }
    return str;
  }

上面用到的一个类


class ErrorMessageModel {
  StateErrorType? errorType;
  String? message;
  int? errorCode;

  ErrorMessageModel({
    this.errorType = StateErrorType.defaultError,
    this.message = "出错啦,请稍后重试~",
    this.errorCode,
  });

  ErrorMessageModel.fromJson(Map<String, dynamic> json) {
    errorType = json['errorType'];
    message = json['message'];
    errorCode = json['errorCode'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = {};
    data['errorType'] = errorType;
    data['message'] = message;
    data['errorCode'] = errorCode;
    return data;
  }
}

配合provider 根据错误不同提示不同页面即可

三 使用

   Future<LoginModel> login(Map<String, dynamic> param) async {
    Response<dynamic> response =
        await DioClient().doPost(Api.login, queryParameters: param);
   LoginModel model =LoginModel.fromJson(response.data);
   return model;
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容