Flutter 网络请求 Dio 封装

6,439 阅读1分钟

一般在dart中网络请求库采用的是dio,在dio之上我们需要对其进行一层封装,以适应业务的需求。

dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等...

1. 首先建一个网络管理工具类DioManager

class DioManager {
  factory DioManager() => getInstance();

  static DioManager get instance => getInstance();
  static DioManager? _instance;

  static DioManager getInstance() {
    if (_instance == null) {
      _instance = DioManager._init();
    }
    return _instance!;
  }

  Dio? _dio;

2. 初始化dio

DioManager._init() {
    if (_dio == null) {
      // 设置 Dio 默认配置
      _dio = Dio(BaseOptions(
          // 请求基地址
          baseUrl: GlobalData.baseUrl,
          // 连接服务器超时时间,单位是毫秒
          connectTimeout: 60 * 1000,
          // 接收数据的最长时限
          receiveTimeout: 60 * 1000));
      String proxy = GlobalData.proxy;
      if (TextUtil.isNotEmpty(proxy)) {
        (_dio!.httpClientAdapter as DefaultHttpClientAdapter)
            .onHttpClientCreate = (client) {
          // config the http client
          client.findProxy = (uri) {
            return "PROXY $proxy";
          };
          //抓Https包设置,这里是默认都通过
          client.badCertificateCallback =
              (X509Certificate cert, String host, int port) => true;
        };
      }

      CookieJar cookieJar = CookieJar();
      // 拦截器,按顺序拦截
      _dio!.interceptors.add(CookieManager(cookieJar));
      _dio!.interceptors.add(OnReqResInterceptors());
      _dio!.interceptors.add(OnErrorInterceptors());
    }
  }

3. JSON 序列化数据

在实战中,后台接口往往会返回一些结构化数据,如JSON。我们可以先将JSON格式转为Dart对象,json.decode() 可以根据JSON字符串具体内容将其转为List或Map,但是这会让我们在运行时才知道具体的类型,这就失去了类型检查的好处了,所以我们还需要将Map或者List转为dart对象,也就是'model'。

首先我们从BaseModel说起,这是项目中后端返回的内容格式,我的项目中BaseModel主要是3个值:

  • responseCode 状态码
  • responseMsg 返回的提示消息
  • responseData 数据

重点是responseData,这是个泛型,先上代码:

class BaseModel<T> {
  int? responseCode;
  String? responseMsg;
  T? responseData;

  BaseModel({this.responseCode, this.responseMsg, this.responseData});

  BaseModel.copy(BaseModel baseModel) {
    this.responseCode = baseModel.responseCode;
    this.responseMsg = baseModel.responseMsg;
  }

  bool get isSuccess {
    return this.responseCode == ErrCode.SUCCESS;
  }

  BaseModel.fromJson(Map<String, dynamic> json, [FromJson<T>? fromJson]) {
    int? responseCode = json['ResponseCode'];
    String? responseMsg = json['ResponseMsg'];
    var data = json['ResponseData'];
    T? responseData;
    try {
      if (data != null) {
        if (fromJson != null) {
          responseData = fromJson(data);
        } else {
          responseData = data;
        }
      }
    } catch (e, stack) {
      throw "responseData解析异常: $e\n$stack";
    }
    this.responseData = responseData;
    this.responseMsg = responseMsg;
    this.responseCode = responseCode;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['ResponseMsg'] = this.responseMsg;
    data['ResponseCode'] = this.responseCode;
    data['ResponseData'] = this.responseData;
    return data;
  }
}

可以看到fromJson的构造方法里有个FromJson的可选参数,这个是用来将从json转为的Map再转为Model的方法。

以下是基础类型的转化:


typedef FromJson<T> = T Function(dynamic json);

FromJson<String> fromStringJson = (dynamic json) => json;
FromJson<int> fromIntJson = (dynamic json) => json;
FromJson<double> fromDoubleJson = (dynamic json) => json;
FromJson<bool> fromBoolJson = (dynamic json) => json;
FromJson<Map<String, dynamic>> fromMapJson = (dynamic json) => json;
FromJson<List<String>> fromListStringJson = fromListJson(fromStringJson);
FromJson<List<int>> fromListIntJson = fromListJson(fromIntJson);
FromJson<List<bool>> fromListBoolJson = fromListJson(fromBoolJson);

FromJson<List<T>> fromListJson<T>(FromJson<T> fromJson) => (dynamic json) {
      List<T> list = [];
      if (json != null && json is List) {
        list = json.map((e) => fromJson(e)).toList();
      }
      return list;
    };

如果你的responseData是个复杂结构,不能用基础类型表示,那么就需要你自己提供一个'FromJson'.

比如结构是这样子:

{
  "squadName" : "Super hero squad",
  "homeTown" : "Metro City",
  "members" : [
    {
      "name" : "Molecule Man",
      "age" : 29,
      "secretIdentity" : "Dan Jukes",
      "powers" : [
        "Radiation resistance",
        "Turning tiny",
        "Radiation blast"
      ]
    }
  ]
}

那你就需要创建一个dart对象,表示这个数据结构,并且提供一个从Map转为这个对象的构造函数。

官方文档提供了两种JSON 序列化数据的方法,一个是手动序列化数据,另一个是利用代码生成进行自动序列化数据。

我都不喜欢,手动太繁琐,利用代码生成虽然有一定的智能但是还需要你手写出所有的成员以及添加注释,我更喜欢直接从json文本直接生成dart对象。

把后端返回的json文本数据拿去一些json2dart的网站,比如:

4. 请求封装

get、post等封装,其实还有put、delete以及下载文件用到的dio.download,在此不列举。 请求参数除了基础的url params和formData以外,还有[isShowErrorToast] 表示出现错误的情况是否自动弹窗提示错误,默认true;[isAddTokenInHeader] 请求头是否添加token; [fromJson]是将json转为model的方法。

约定所有的成功、错误、异常等各种情况都会返回一个BaseModel,请求后的业务代码需要调用BaseModel实例的isSuccess方法来判断是否请求成功。

/// Dio 请求方法

enum DioMethod {
  get,
  post,
  put,
  delete,
}

/// get请求
/// [isShowErrorToast] 出现错误的情况是否自动弹窗提示错误,默认true
/// [isAddTokenInHeader] 请求头是否添加token
/// [fromJson]是将json转为model的方法
Future get<T>(
  {required String url,
  bool isShowErrorToast = true,
  bool isAddTokenInHeader = true,
  Map<String, dynamic>? params,
  FromJson<T>? fromJson}) async {
return await requestHttp(url,
    method: DioMethod.get,
    isShowErrorToast: isShowErrorToast,
    params: params,
    fromJson: fromJson);
}

/// post 请求
Future post<T>(
  {required String url,
  Map<String, dynamic>? params,
  bool isAddTokenInHeader = true,
  bool isShowErrorToast = true,
  FormData? formData,
  FromJson<T>? fromJson,
  CancelToken? cancelToken,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress}) async {
return await requestHttp<T>(url,
    method: DioMethod.post,
    isShowErrorToast: isShowErrorToast,
    params: params,
    formData: formData,
    fromJson: fromJson,
    cancelToken: cancelToken,
    onSendProgress: onSendProgress,
    onReceiveProgress: onReceiveProgress);
}

/// Dio request 方法
Future requestHttp<T>(String url,
  {DioMethod method = DioMethod.get,
  Map<String, dynamic>? params,
  bool isShowErrorToast = true,
  bool isAddTokenInHeader = true,
  FormData? formData,
  FromJson<T>? fromJson,
  CancelToken? cancelToken,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress}) async {
const methodValues = {
  DioMethod.get: 'get',
  DioMethod.post: 'post',
  DioMethod.delete: 'delete',
  DioMethod.put: 'put'
};

try {
  Response response;

  /// 不同请求方法,不同的请求参数,按实际项目需求分.
  /// 这里 get 是 queryParameters,其它用 data. FormData 也是 data
  /// 注意: 只有 post 方法支持发送 FormData.
  switch (method) {
    case DioMethod.get:
      response = await _dio!.request(url,
          queryParameters: params,
          options: Options(method: methodValues[method], extra: {
            'isAddTokenInHeader': isAddTokenInHeader,
            'isShowErrorToast': isShowErrorToast
          }));
      break;
    default:
      // 如果有formData参数,说明是传文件,忽略params的参数
      if (formData != null) {
        response = await _dio!.post(url,
            data: formData,
            cancelToken: cancelToken,
            onSendProgress: onSendProgress,
            onReceiveProgress: onReceiveProgress);
      } else {
        response = await _dio!.request(url,
            data: params,
            cancelToken: cancelToken,
            options: Options(method: methodValues[method], extra: {
              'isAddToken': isAddTokenInHeader,
              'isShowErrorToast': isShowErrorToast
            }));
      }
  }
  // json转model
  String jsonStr = json.encode(response.data);
  Map<String, dynamic> responseMap = json.decode(jsonStr);
  BaseModel<T> baseModel = BaseModel.fromJson(responseMap, fromJson);

  // 处理用户被踢出的情况
  if (baseModel.responseCode == ErrCode.USER_OUT) {
    // TODO ...
    return baseModel;
  }

  if (baseModel.responseCode != ErrCode.SUCCESS) {
    if (isShowErrorToast) {
      EasyLoading.showToast(baseModel.responseMsg ?? '请求出错');
    }
  }
  return baseModel;
} on DioError catch (e) {
  // DioError是指返回值不为200的情况
  logger.shout('DioError报错${e.type}:${e.error.toString()}');
  // 对错误进行判断
  onErrorInterceptor(e);
  // 判断是否断网了
  String? errorMsg = isNetworkConnected
      ? e.requestOptions.extra["errorMsg"]
      : "网络连接断开,请检查网络设置";
  return BaseModel<T>(
      responseCode: ErrCode.NETWORK_ERR, responseMsg: errorMsg);
} catch (e) {
  // 其他一些意外的报错
  logger.shout('Dio报错:${e.toString()}');
  return BaseModel<T>(responseCode: ErrCode.OTHER_ERR, responseMsg: "其他异常");
}
}

// 错误判断
void onErrorInterceptor(DioError err) {
  // 异常分类
  switch (err.type) {
    // 4xx 5xx response
    case DioErrorType.response:
      err.requestOptions.extra["errorMsg"] = err.response?.data ?? "连接异常";
      break;
    case DioErrorType.connectTimeout:
      err.requestOptions.extra["errorMsg"] = "连接超时";
      break;
    case DioErrorType.sendTimeout:
      err.requestOptions.extra["errorMsg"] = "发送超时";
      break;
    case DioErrorType.receiveTimeout:
      err.requestOptions.extra["errorMsg"] = "接收超时";
      break;
    case DioErrorType.cancel:
      err.requestOptions.extra["errorMsg"] =
          err.message.isNotEmpty ? err.message : "取消连接";
      break;
    case DioErrorType.other:
    default:
      err.requestOptions.extra["errorMsg"] = "连接异常";
      break;
  }
}
  1. 拦截器

主要是在请求之前给header添加一些指定的参数。 如果header的值是比较固定的,比如不需要时间戳参数进行运算的,那么其实可以在初始化dio的时候传入BaseOptions。

/// 请求拦截
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
String token = getToken();
if (token.isNotEmpty && options.extra['isAddTokenInHeader'] != false) {
  options.headers['Token'] = token;
  int t = DateTime.now().millisecondsSinceEpoch;
  options.headers['key'] =/* 加密 */;
  options.headers['timestamp'] = t;
}

options.headers['deviceId'] = getDeviceId();

return super.onRequest(options, handler);

}

最后

如果有纰漏、错误,欢迎拍砖交流~