Flutter 开发中,Json 数据解析一直是一个痛点,特别是对于从 iOS、Android 或者 Java 转过来的开发者来说尤为明显,在上述平台上开发者习惯了将 Json 数据解析为对象实体然后进行使用,而在 Flutter 上要做到这一步则相对比较麻烦。
Flutter 使用的是 Dart 语言进行开发,而 Dart 语言没有反射,所以无法像 Java 一样通过反射直接将 Json 数据映射为对应的对象实体类对象。官方解决方案是将 Json 数据转换为字典,然后从字典中进行取数使用。但直接从字典中取数很不方便,写代码时没有自动提示很不友好,而且可能在写的时候写错字段名。
基于 Flutter 现状,方便开发时的调用,可以将 Json 转换为字典后再手动映射到对象实体字段里,这样使用时就可以直接使用对应实体类对象,但是这种方法会导致开发过程中写很多冗余代码,因为每一个类都要手动去写对应的映射代码,严重影响开发效率。于是就有了很多将 Json 映射为对象实体类代码的自动生成方案,比如 Json2Dart、JsonToDart、Json To Dart Class 、FlutterJsonBeanFactory 等插件以及 json_to_model 之类的第三方库。其本质原理就是将需要开发者手动编写的映射代码改为自动生成。
笔者经过不断的尝试、实验,发现这些方案或多或少都存在着一些美中不足,经过不断权衡比较再结合实际开发中的使用情况,最后选择了使用 FlutterJsonBeanFactory
插件再加上一些自定义的代码修改,最终达到在项目中快速使用的效果。
接下来本文将主要讲解怎么使用 FlutterJsonBeanFactory
插件结合自定义代码修改,快速实现 Json 解析。
0. 插件安装
在 Android Studio 插件市场里找到 FlutterJsonBeanFactory
进行安装。
安装完后记得重启一下 Android Studio ,否则可能会出现无法生成代码的情况。如果重启后还是无法生成则采用
File
->Invalidate Caches/Restart...
这种方式重启一下。
重启后在项目目录上右键 New
下能看到一个 JsonToDartBeanAction
的菜单说明就安装成功了。
1. 创建实体类
1.1 创建
在目录上点击 New
=> JsonToDartBeanAction
菜单后弹出创建 Model Class 的界面,如下图:
•Class Name :要创建的类的名称•JSON Text :类对应 Json 的示例数据•null-able :是否空安全,不勾选生成的字段都为非空类型,勾选以后生成的字段则全为可空类型
在该界面填入要创建 Class 的名称以及对应类的 Json 示例数据,点击 Make 即可生成对应 Class 代码。
生成的实体类及对应文件名称默认加了 entitiy
后缀,如果不需要或者要修改为其他后缀可在插件设置里进行设置:
生成以后的目录结构如下:
•
models
为项目自建目录,即右键选择创建实体类的目录,生成的实体类存放在该目录;
•
generated/json
为插件生成目录,其中 xxx_entity.g.dart
根据实体类生成的类辅助方法,base
目录下为基础公共代码
下面将对生成的每个文件做一个详细解析。
1.2 xxx_entity.dart
插件会在目标目录下生成 xxx_entity.dart
文件,即对应实体类文件,包含实体类的代码。如上面创建的 User 则会生成 user_entity.dart
, 对应实体类为 UserEntity
如下:
@JsonSerializable()
class UserEntity {
String? id;
String? name;
int? age;
UserEntity();
factory UserEntity.fromJson(Map<String, dynamic> json) => $UserEntityFromJson(json);
Map<String, dynamic> toJson() => $UserEntityToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}
插件会自动生成实体类对应字段,如果选择了 null-able
则字段类型为可空类型即类型后会有一个 ?
。
除了字段以外还会生成 fromJson 的工厂方法以及 toJson 方法,用于通过 Json 转换为实体类以及将实体类转换为 Json。对应调用的方法为 XxxEntityFromJson 和 XxxEntityToJson ,对应方法的代码实现在 .g.dart 文件中
最后重写了 toString
方法,实现将实体转换为 Json 字符串。
生成的实体类使用 @JsonSerializable()
进行注解标记,后续重新生成代码时会查找该注解的实体类进行生成。
生成实体类后如果要对实体字段进行修改,比如增加字段或者修改字段类型、名称等,修改完以后后使用
Alt J
即可重新生成对应的代码。
1.3 xxx_entity.g.dart
xxx_entity.g.dart 为实体类对应的辅助方法文件,存放在 generated/json 目录下,以.g.dart 为后缀。主要包含 XxxFromJson 和 XxxToJson 两个方法,以 $
实体类名
为前缀,生成内容如下:
UserEntity $UserEntityFromJson(Map<String, dynamic> json) {
final UserEntity userEntity = UserEntity();
final String? id = jsonConvert.convert<String>(json['id']);
if (id != null) {
userEntity.id = id;
}
final String? name = jsonConvert.convert<String>(json['name']);
if (name != null) {
userEntity.name = name;
}
final int? age = jsonConvert.convert<int>(json['age']);
if (age != null) {
userEntity.age = age;
}
return userEntity;
}
Map<String, dynamic> $UserEntityToJson(UserEntity entity) {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = entity.id;
data['name'] = entity.name;
data['age'] = entity.age;
return data;
}
•XxxFromJson 将 Json 数据的对应字段取出来然后赋值给实体类的对应字段。Json 数据转换为实体字段使用了 jsonConvert.convert 其定义在 json_convert_content.dart 中。•XxxToJson 将实体数据转换为 Map 字典。
1.4 json_convert_content.dart
json_convert_content.dart
为 JsonConvert
类, 用于统一进行 Json 与实体类的转换,存放目录为 generated/json/base
, 生成内容如下:
JsonConvert jsonConvert = JsonConvert();
class JsonConvert {
T? convert<T>(dynamic value) {...}
List<T?>? convertList<T>(List<dynamic>? value) {...}
List<T>? convertListNotNull<T>(dynamic value) {...}
T? asT<T extends Object?>(dynamic value) {...}
static M? _fromJsonSingle<M>(Map<String, dynamic> json) {...}
static M? _getListChildType<M>(List<dynamic> data) {...}
static M? fromJsonAsT<M>(dynamic json) {...}
}
在文件开头创建了一个全局的 jsonConvert
变量,方便在其他地方直接调用。
下面将对 JsonConvert
每个方法的作用做一个详细的介绍:
convert
convert
是将 Json 数据转换为实体对象,源码如下:
T? convert<T>(dynamic value) {
if (value == null) {
return null;
}
return asT<T>(value);
}
代码很简单,首先判断了传入的数据是否为 null
,为 null
则直接返回 null
, 不为空则调用 asT
方法。在生成的 .g.dart
的 $UserEntityFromJson
方法中非 List 类型字段基本都是调用 convert
方法进行转换。
convertList
convertList
是将 Json 数据转换为实体对象 List, 源码如下:
List<T?>? convertList<T>(List<dynamic>? value) {
if (value == null) {
return null;
}
try {
return value.map((dynamic e) => asT<T>(e)).toList();
} catch (e, stackTrace) {
print('asT<$T> $e $stackTrace');
return <T>[];
}
}
代码也很简单,首先也是判断了传入的数据是否为 null
,为 null
则直接返回 null
, 不为空则遍历 value 使用 map
调用 asT
方法进行转换,最终还是调用的 asT
方法。在转换上加了 try-catch
如果报错则返回空的 List。
convertListNotNull
convertListNotNull
与 convertList
作用相同,也是将 Json 数据转换为实体 List ,源码如下:
List<T>? convertListNotNull<T>(dynamic value) {
if (value == null) {
return null;
}
try {
return (value as List<dynamic>).map((dynamic e) => asT<T>(e)!).toList();
} catch (e, stackTrace) {
print('asT<$T> $e $stackTrace');
return <T>[];
}
}
与 convertList
的区别是参数不一样,convertList
参数传入的是 List<dynamic>
而 convertListNotNull
传入的直接是dynamic
。其次最大的区别是调用 asT
方法时 convertListNotNull
在 asT
后面加了一个 !
,表示不为空。
当在实体类里定义字段为 List
类型时,会根据是否为非空类型而选择生成 convertList
或 convertListNotNull
来进行转换:
• List<Xxxx?>?
: 当定义 List 为可空类型,且 List 里元素的类型也为可空类型时,使用 convertList
• List<Xxxx>?
: 当定义 List 为可空类型,但 List 里元素的类型为非空类型时,使用 convertListNotNull
• List<Xxxx>?
: 当定义 List 为非空类型,且 List 里元素的类型也为非空类型时,使用 convertListNotNull
asT
convert
、convertList
、 convertListNotNull
最终调用的都是 asT
方法,源码如下:
T? asT<T extends Object?>(dynamic value) {
if (value is T) {
return value;
}
final String type = T.toString();
try {
final String valueS = value.toString();
if (type == "String") {
return valueS as T;
} else if (type == "int") {
final int? intValue = int.tryParse(valueS);
if (intValue == null) {
return double.tryParse(valueS)?.toInt() as T?;
} else {
return intValue as T;
} } else if (type == "double") {
return double.parse(valueS) as T;
} else if (type == "DateTime") {
return DateTime.parse(valueS) as T;
} else if (type == "bool") {
if (valueS == '0' || valueS == '1') {
return (valueS == '1') as T;
}
return (valueS == 'true') as T;
} else {
return JsonConvert.fromJsonAsT<T>(value);
}
} catch (e, stackTrace) {
print('asT<$T> $e $stackTrace');
return null;
}
}
相对于上面三个方法,asT
方法的代码较多一些,但其实也很简单。
首先判断传入的数据类型是否为要转换的数据类型,如果是的话就直接返回传入参数,即如果要将传入数据转换为 User
,但是传入参数本身就是 User
类型,那就直接返回。
然后通过 T.toString()
获取泛型类型的名称,再与 String
、int
、double
、DateTime
、bool
这些基础数据类型进行比较,如果是这些类型则调用这些类型的转换方法进行转换。
最后,如果不是基础类型则调用 fromJsonAsT
方法。
fromJsonAsT
代码语言:javascript复制static M? fromJsonAsT<M>(dynamic json) {
if(json == null){
return null;
}
if (json is List) {
return _getListChildType<M>(json);
} else {
return _fromJsonSingle<M>(json as Map<String, dynamic>);
}
}
判断传入 Json 数据是否为 null
, 为 null
则直接返回 null
。然后判断 Json 数据是否为 List
,是 List 则调用 _getListChildType
否则调用 _fromJsonSingle
。
_fromJsonSingle
_fromJsonSingle
为单个实体对象的转换,源码如下:
static M? _fromJsonSingle<M>(Map<String, dynamic> json) {
final String type = M.toString();
if(type == (UserEntity).toString()){
return UserEntity.fromJson(json) as M;
}
print("$type not found");
return null;
}
首先通过 M.toString()
方法获取泛型的类型名称,然后与生成的实体类型进行比较,相同则调用对应实体类的 fromJson
方法。比如这里的 UserEntity
, 判断泛型类型名称与 UserEntity.toString()
相等,则调用 UserEntity.fromJson
。如果通过插件创建了多个实体类,则这里就会存在多个类似的 if 判断语句。
_getListChildType
_getListChildType
为转换 List 数据,源码如下:
static M? _getListChildType<M>(List<dynamic> data) {
if(<UserEntity>[] is M){
return data.map<UserEntity>((e) => UserEntity.fromJson(e)).toList() as M;
}
print("${M.toString()} not found");
return null;
}
与 _fromJsonSingle
不同,这里不是使用的泛型类型名称判断,而是直接创建对应实体类的空 List 判断是否为泛型类型,如上面的 <UserEntity>[] is M
。如果类型相同,则通过 map 调用对应实体类的 fromJson
方法进行转换。同样的如果创建了多个实体类,这里也会存在多个类似的 if 判断语句。
所以最终其实是调用实体类的 fromJson
方法,而该方法则调用的是 xxxx_entity.g.dart
里生成的 $UserEntityFromJson
方法。
整体流程如下:
1.5 json_field.dart
包含 JsonSerializable
和 JSONField
两个注解。存放目录为 generated/json/base
class JsonSerializable{
const JsonSerializable();
}
class JSONField {
//Specify the parse field name
final String? name;
//Whether to participate in toJson
final bool? serialize;
//Whether to participate in fromMap
final bool? deserialize;
const JSONField({this.name, this.serialize, this.deserialize});
}
•JsonSerializable 类注解,二次生成代码时插件查找该注解的类进行生成。•JSONField 字段注解,用于自定义字段映射和配置是否序列化和反序列化字段
2. 使用
2.1 单实体解析
直接调用实体类对应的 fromJson
方法即可将 Json 数据解析为实体对象。
String userData = """
{
"id":"12313",
"name":"loongwind",
"age":18
}
""";
UserEntity user = UserEntity.fromJson(jsonDecode(userData));
fromJson 需要的参数是 Map ,所以需要先使用 jsonDecode 将 Json 字符串转换为 Map
除了直接使用实体类的 fromJson
方法外也可以直接使用生成的 JsonConvert
来解析:
String userData = """
{
"id":"12313",
"name":"loongwind",
"age":18
}
""";
UserEntity? user = jsonConvert.convert<UserEntity>(jsonDecode(userData));
UserEntity? user = jsonConvert.asT<UserEntity>(jsonDecode(userData));
UserEntity? user = JsonConvert.fromJsonAsT<UserEntity>(jsonDecode(userData));
使用 JsonConvert
的 convert
、 asT
、fromJsonAsT
方法可以实现相同的解析效果。
2.2 List 解析
解析 Json List 数据则需要调用 JsonConvert
的对应方法进行解析,除了使用上面的 convert
、asT
、fromJsonAsT
外,还可以使用 convertList
、convertListNotNull
:
String userData = """
[
{
"id":"12313",
"name":"loongwind",
"age":18
},
{
"id":"22222",
"name":"cmad",
"age":25
}
]
""";
List<UserEntity>? users = jsonConvert.convert<List<UserEntity>>(jsonDecode(userData));
List<UserEntity>? users = jsonConvert.asT<List<UserEntity>>(jsonDecode(userData));
List<UserEntity>? users = JsonConvert.fromJsonAsT<List<UserEntity>>(jsonDecode(userData));
List<UserEntity?>? users = jsonConvert.convertList<UserEntity>(jsonDecode(userData));
List<UserEntity>? users = jsonConvert.convertListNotNull<UserEntity>(jsonDecode(userData));
convertList
、convertListNotNull
与 convert
、asT
、fromJsonAsT
的区别在于前者的泛型为 List Item元素的泛型类型,后者则直接为对应 List 的类型。如上面 convertList
、convertListNotNull
的泛型直接为 UserEntity
, 而 convert
、asT
、fromJsonAsT
的泛型为 List<UserEntity>
。
2.3 JSONField 的使用
自定义字段名
实际开发中可能会存在 Json 数据字段与代码中的字段不一致的情况,比如 Json 中的字段命名不符合代码规范,这个时候就可以使用 JSONField 来实现自定义的字段映射。
如 Json 里的字段为 AGE
需要映射到实体类的 age
字段,只需要在实体类的 age 字段上加上 JSONField 注解,指定 name 为 AGE
, 然后使用 Alt
J
重新生成代码:
String? id;
String? name;
@JSONField(name: "AGE")
int? age;
代码语言:javascript复制添加 @JSONField 注解后一定要使用
Alt
J
重新生成代码,否则不生效
String userData = """
{
"id":"12313",
"name":"loongwind",
"AGE":18
}
""";
UserEntity user = UserEntity.fromJson(jsonDecode(userData));
print(user.age) // 18
这样就能实现 AGE 与 age 字段的映射。
忽略字段
JSONField 还有两个字段 serialize
、 deserialize
用于序列化和反序列化时忽略某个字段,比如不需要解析 name 字段则可设置 deserialize
为 false ,如果 toJson 时不需要序列化某个字段,则设置 serialize
为 false。
@JSONField(deserialize: false)
String? name;
//------
String userData = """
{
"id":"12313",
"name":"loongwind",
"AGE":18
}
""";
UserEntity user = UserEntity.fromJson(jsonDecode(userData));
print(user.name); // null
@JSONField(serialize: false)
String? name;
//------
UserEntity user = UserEntity();
user.id = "123";
user.name = "loongwind";
user.age = 18;
print(user.toJson()); // {id: 123, AGE: 18}
当字段设置 @JSONField(deserialize: false)
时即使 Json 数据有该字段也不会进行解析,打印字段值为 null ,同样的如果设置 @JSONField(serialize: false)
时,当调用 toJson
时,即使字段有值转换为 Json 数据也不会有该字段。
3. 优化
上面已经讲解了使用插件生成实体类后如何进行 Json 数据解析的基本使用,但是在实际项目开发过程中会存在一定的问题,实际项目开发中接口返回的数据格式一般是这样的:
代码语言:javascript复制{
"code": 200,
"message": "success",
"data":{
"id": "12312312",
"name": "loongwind",
"age": 18
}
}
在返回数据外又统一包裹了一层,data 字段的数据才是实际业务需要的数据,而不同的接口返回的 data 数据结构也不相同,如果直接使用插件生成的,会生成如下代码:
代码语言:javascript复制@JsonSerializable()
class UserResponseEntity {
int? code;
String? message;
UserResponseData? data;
UserResponseEntity();
//...
}
@JsonSerializable()
class UserResponseData {
String? id;
String? name;
int? age;
UserResponseData();
//...
}
这样的话每一个接口都要生成一个 ResponseEntity 类,使用起来也不方便不便于统一封装。所以需要对 ResponseEntity 进行改造,让其支持泛型解析。
首先重新使用上面的 Json 示例数据生成一个 ApiResponseEntity
,然后将 data 字段类型改为 dynamic
,使用 Alt
J
重新生成代码:
@JsonSerializable()
class ApiResponseEntity {
int? code;
String? message;
dynamic data;
ApiResponseEntity();
factory ApiResponseEntity.fromJson(Map<String, dynamic> json) => $ApiResponseEntityFromJson(json);
Map<String, dynamic> toJson() => $ApiResponseEntityToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}
再将 @JsonSerializable()
注解去掉,把 api_response_entity.dart
和 api_response_entity.g.dart
文件放到一个单独的文件夹内
前面说了使用
Alt
J
重新生成代码会根据@JsonSerializable()
注解生成,因为需要修改ApiResponseEntity
类来满足泛型解析的需求,所以要去除@JsonSerializable()
注解防止重新生成代码将自定义代码覆盖掉。而去掉了@JsonSerializable()
注解后,下次生成代码时会自动删除generated/json
下多余的.g.dart
,所以需要将其拷贝到其他目录防止下次生成时被删除。
最后给 ApiResponseEntity
以及 ApiResponseEntityFromJson
添加泛型支持。修改后内容如下:
class ApiResponseEntity<T> {
int? code;
String? message;
T? data;
ApiResponseEntity();
factory ApiResponseEntity.fromJson(Map<String, dynamic> json) => $ApiResponseEntityFromJson<T>(json);
Map<String, dynamic> toJson() => $ApiResponseEntityToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}
ApiResponseEntity<T> $ApiResponseEntityFromJson<T>(Map<String, dynamic> json) {
final ApiResponseEntity<T> apiResponseEntity = ApiResponseEntity<T>();
final int? code = jsonConvert.convert<int>(json['code']);
if (code != null) {
apiResponseEntity.code = code;
}
final String? message = jsonConvert.convert<String>(json['message']);
if (message != null) {
apiResponseEntity.message = message;
}
final T? data = jsonConvert.convert<T>(json['data']);
if (data != null) {
apiResponseEntity.data = data;
}
return apiResponseEntity;
}
给 ApiResponseEntity
上加上泛型 T
,然后修改 data 类型为 T?
, 再给 $ApiResponseEntityFromJson
方法上添加泛型,解析 data 数据的时候就可以直接使用 jsonConvert.convert<T>
进行解析。
修改完后使用示例如下:
•单实体解析
代码语言:javascript复制String userData = """
{
"code": 200,
"message": "success",
"data":{
"id": "12312312",
"name": "loongwind",
"age": 18
}
}
""";
ApiResponseEntity<UserEntity> response = ApiResponseEntity.fromJson(jsonDecode(userData));
print(response.data?.name); // loongwind
•List 解析
代码语言:javascript复制String userData = """
{
"code": 200,
"message": "success",
"data":[
{
"id": "12312312",
"name": "loongwind",
"age": 18
},{
"id": "333333",
"name": "cmad",
"age": 25
}
]
}
""";
ApiResponseEntity<List<UserEntity>> response = ApiResponseEntity.fromJson(jsonDecode(userData));
print(response.data?.length); // 2
print(response.data?.first.name); // loongwind
•基本数据类型解析
代码语言:javascript复制String jsonData = """
{
"code": 200,
"message": "success",
"data": 18
}
""";
ApiResponseEntity<int> response = ApiResponseEntity.fromJson(jsonDecode(jsonData));
print(response.data); // 18
String jsonData = """
{
"code": 200,
"message": "success",
"data": "123456"
}
""";
ApiResponseEntity<String> response = ApiResponseEntity.fromJson(jsonDecode(jsonData));
print(response.data); // 123456
String jsonData = """
{
"code": 200,
"message": "success",
"data": true
}
""";
ApiResponseEntity<bool> response = ApiResponseEntity.fromJson(jsonDecode(jsonData));
print(response.data); // true
经过上面的改造以后,ApiResponseEntity
则满足项目开发中使用。