Flutter 网络请求dio与json泛型解析

2,706 阅读5分钟

dio

详细中文文档点这里查看

执行流

请求拦截器Interceptor >> 请求转换器Transformer >> 发起请求 >> 响应转换器Transformer >> 响应拦截器Interceptor >> 最终结果。

拦截器Interceptor

Interceptor有三个方法,分别表示请求拦截onRequest,响应拦截onResponse,错误拦截onError.

一个dio对象会有多个Interceptor,它们的执行顺序是FIFO,先加入的拦截器会先执行,后面的拦截器,根据前面拦截器的InterceptorHandler对象的调用方法来判断执行操作;

一般不在Interceptor流程里面修改请求返回的数据格式,如果需要修改,可以在Transformer里面进行处理

QueuedInterceptor序列拦截器

如果有多个并发请求,则在进入拦截器之前将请求添加到队列中。每次只有一个请求进入拦截器,在该请求被拦截器处理后,下一个请求将进入拦截器。不调用InterceptorHandler的方法时,其它请求会一直等待.

使用场景:请求前判断token是否为空,为空的话请求token并插入,再执行next.

class Interceptors extends ListMixin<Interceptor> {
  final _list = <Interceptor>[];
  final Lock _requestLock = Lock();
  final Lock _responseLock = Lock();
  final Lock _errorLock = Lock();

根据代码看,每个流程都有单独的锁,每个dio对象,都有单独的Interceptors实例,各个流程之间互不影响,各个dio对象之间,也互不影响?(待确认)

拦截器的流程控制器 InterceptorHandler

InterceptorHandler有三个方法: next,resolve,reject.

next

继续执行下个拦截器的相同拦截方法,比如在onRequest里面调用handler.next(options),那么下一个拦截器的onRequest也会被执行,依次下去.

通常会调用这个,便于拦截器依次执行,避免出现某些拦截器没被执行导致出错.

resolve

结束拦截器流,直接返回结果; RequestInterceptorHandler 调用resolve时,可以传入callFollowingResponseInterceptor = true来控制是否执行后续拦截器的onResponse方法.

比如get请求的缓存,可以在onRequest里面判断是否有缓存,如果有,直接调用resolve,返回缓存的结果,而不用发起请求.

reject

抛出错误 ,结束拦截器流程,此时请求流程也会结束,抛出错误

RequestInterceptorHandlerResponseInterceptorHandler 调用reject时,可以传入callFollowingErrorInterceptor = true来控制是否执行后续拦截器的onError方法.

比如发起请求前先进行网络环境判断,如果没有网络,则直接调用reject,抛出错误,略过dio执行流的过程. 比如收到请求后,判断response.data里面的服务器请求码,如果失败则使用reject,将response流程转到error流程.

转换器Transformer

dio自带了一个转换器DefaultTransformer,它处理了请求时数据的编码,和请求响应数据的解析.

  1. 处理RequestOptions里的data数据,将其它类型的data转换成String
  2. 处理请求收到的ResponseBody,接收完整的二进制流ResponseBody.stream

DefaultTransformer 预留了一个属性jsonDecodeCallback,可以自定义把数据从string转换成dynamic类型的过程:

typedef JsonDecodeCallback = dynamic Function(String);
JsonDecodeCallback? jsonDecodeCallback;

如果不提供jsonDecodeCallback属性,那么默认会使用json.decode(responseBody)来转换.

if (responseBody.isNotEmpty &&
  options.responseType == ResponseType.json &&
  Transformer.isJsonMimeType(
      response.headers[Headers.contentTypeHeader]?.first)) {
   final callback = jsonDecodeCallback;
   if (callback != null) {
     return callback(responseBody);
   } else {
     return json.decode(responseBody);
   }
}

仅针对json类型的数据才会调用jsonDecodeCallback进行处理.

使用场景:

  • 超大json解析时,会占用main isolate导致UI卡顿,这个时候可以使用compute函数来进行json解析;同时dio文档上有注明,使用compute会导致json解析耗时变长.
  • 可以直接将json字符串解析成请求响应的base模型数据(BaseEntity<T>).

实际使用时,不能一步到位直接拿到解析数据,只能拿到BaseEntity<dynamic>类型的数据,然后拿到数据后还得进行第二次解析,将dynamic解析成想要的T.而由于jsonDecodeCallback不支持泛型,二次解析,还得在执行流的其他环节处理.这样会导致整个解析逻辑分散.

多文件上传

多文件上传时,通过给key加中括号[]方式作为文件数组的标记,大多数后台也会通过key[]这种方式来读取。不过RFC中并没有规定多文件上传就必须得加[],所以有时不带[]也是可以的,关键在于后台和客户端得一致。v3.0.0 以后通过Formdata.fromMap()创建的Formdata,如果有文件数组,是默认会给key加上[]的,比如:

FormData.fromMap({
  'files': [
    MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
    MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
  ]
});

最终编码时会key会为 files[],如果不想添加[],可以通过Formdata的API来构建:

var formData = FormData();
formData.files.addAll([
  MapEntry('files',
    MultipartFile.fromFileSync('./example/upload.txt',filename: 'upload.txt'),
  ),
  MapEntry('files',
    MultipartFile.fromFileSync('./example/upload.txt',filename: 'upload.txt'),
  ),
]);

这样构建的FormData的key是不会有[]

json解析库

用于将后端返回的json数据转换成dart对象

  json_annotation: ^4.5.0
  json_serializable: ^6.2.0

app的需求是,每次返回的最外层模型是一致的,只有data每次不一样,而且只有data字段才是业务字段,其它字段仅判断请求是否成功。这里需要使用泛型来进行转换。

手动使用太过麻烦,一般使用插件。而VSCode里面没有比较好的插件。

FlutterJsonBeanFactory 插件

Android Studio里面才有这个插件。处理json文件时最好使用Android Studio

使用时直接将后台返回的json复制到插件页面,填好名称后就能自动生成对应的entity.dartentity.g.dart文件。

通过插件生成文件之后,lib/generated/json/base文件夹下会生成两个文件:

json_convert_content.dart

这个文件专门负责将json数据(Map或者List)转换成泛型T或者[T],因为dart不支持反射,所以没法像其它语言那样使用反射来处理json。也就是说,传入的T需要调用T.fromJson()方法时,是没法通过反射来调用的。

json_convert_content提供了一个_convertFuncMap,专门存放模型T与对应的fromJson方法:

  static final Map<String, JsonConvertFunction> _convertFuncMap = {
    (AppVersionEntity).toString(): AppVersionEntity.fromJson,
  };

这样在json转模型时,直接通过T.toString()作为key,来查找对应的fromJson方法。

_convertFuncMap只处理了Map -> Model的流程,如果json是一个List<Map>,显然没有List<T>.fromJson()方法可以调用。

json_convert_content提供了一个_getListChildType方法,专门负责将List<Map>转换成List<T>:

  static M? _getListChildType<M>(List<Map<String, dynamic>> data) {
    if(<AppVersionEntity>[] is M){
      return data.map<AppVersionEntity>((Map<String, dynamic> e) => AppVersionEntity.fromJson(e)).toList() as M;
    }

    debugPrint("${M.toString()} not found");
  
    return null;
}

使用插件时,插件会自动管理json_convert_content文件,将创建的Entity类型加入上面_convertFuncMap_getListChildType里面,避免手动管理出现错漏。

json_field.dart

json序列化的工具类,决定json转模型的字段映射,以及是否映射

class JsonSerializable{
    const JsonSerializable();
}

class JSONField {
  //自定义映射名称
  final String? name;

  //转json时是否映射
  final bool? serialize;
  
  //转模型时是否映射
  final bool? deserialize;

  const JSONField({this.name, this.serialize, this.deserialize});
}

一般也不用自己处理,插件生成的文件自己会处理好,后续有特殊情况才需要手动修改。

示例:

@JsonSerializable() //标记这个类是json序列化来的
class AppVersionEntity {

  int? id;
  @JSONField(name: "source") //自定义映射名,json里面是source,模型里面是xSource
  String? xSource;
  String? version;
  String? versionName;
  String? url;
  String? title;
  String? updateContent;
  bool? status;
  String? channel;
  String? gmtCreate;
  String? gmtUpdate;
  
  AppVersionEntity();

  factory AppVersionEntity.fromJson(Map<String, dynamic> json) => $AppVersionEntityFromJson(json);

  Map<String, dynamic> toJson() => $AppVersionEntityToJson(this);

  @override
  String toString() {
    return jsonEncode(this);
  }
}