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
如下:
1 | () |
插件会自动生成实体类对应字段,如果选择了 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
两个方法,以 $
+实体类名
为前缀,生成内容如下:
1 | UserEntity $UserEntityFromJson(Map<String, dynamic> json) { |
$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
, 生成内容如下:
1 | JsonConvert jsonConvert = JsonConvert(); |
在文件开头创建了一个全局的 jsonConvert
变量,方便在其他地方直接调用。
下面将对 JsonConvert
每个方法的作用做一个详细的介绍:
convert
convert
是将 Json 数据转换为实体对象,源码如下:
1 | T? convert<T>(dynamic value) { |
代码很简单,首先判断了传入的数据是否为 null
,为 null
则直接返回 null
, 不为空则调用 asT
方法。在生成的 .g.dart
的 $UserEntityFromJson
方法中非 List 类型字段基本都是调用 convert
方法进行转换。
convertList
convertList
是将 Json 数据转换为实体对象 List, 源码如下:
1 | List<T?>? convertList<T>(List<dynamic>? value) { |
代码也很简单,首先也是判断了传入的数据是否为 null
,为 null
则直接返回 null
, 不为空则遍历 value 使用 map
调用 asT
方法进行转换,最终还是调用的 asT
方法。在转换上加了 try-catch
如果报错则返回空的 List。
convertListNotNull
convertListNotNull
与 convertList
作用相同,也是将 Json 数据转换为实体 List ,源码如下:
1 | List<T>? convertListNotNull<T>(dynamic value) { |
与 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
方法,源码如下:
1 | T? asT<T extends Object?>(dynamic value) { |
相对于上面三个方法,asT
方法的代码较多一些,但其实也很简单。
首先判断传入的数据类型是否为要转换的数据类型,如果是的话就直接返回传入参数,即如果要将传入数据转换为 User
,但是传入参数本身就是 User
类型,那就直接返回。
然后通过 T.toString()
获取泛型类型的名称,再与 String
、int
、double
、DateTime
、bool
这些基础数据类型进行比较,如果是这些类型则调用这些类型的转换方法进行转换。
最后,如果不是基础类型则调用 fromJsonAsT
方法。
fromJsonAsT
1 | static M? fromJsonAsT<M>(dynamic json) { |
判断传入 Json 数据是否为 null
, 为 null
则直接返回 null
。然后判断 Json 数据是否为 List
,是 List 则调用 _getListChildType
否则调用 _fromJsonSingle
。
_fromJsonSingle
_fromJsonSingle
为单个实体对象的转换,源码如下:
1 | static M? _fromJsonSingle<M>(Map<String, dynamic> json) { |
首先通过 M.toString()
方法获取泛型的类型名称,然后与生成的实体类型进行比较,相同则调用对应实体类的 fromJson
方法。比如这里的 UserEntity
, 判断泛型类型名称与 UserEntity.toString()
相等,则调用 UserEntity.fromJson
。如果通过插件创建了多个实体类,则这里就会存在多个类似的 if 判断语句。
_getListChildType
_getListChildType
为转换 List 数据,源码如下:
1 | static M? _getListChildType<M>(List<dynamic> data) { |
与 _fromJsonSingle
不同,这里不是使用的泛型类型名称判断,而是直接创建对应实体类的空 List 判断是否为泛型类型,如上面的 <UserEntity>[] is M
。如果类型相同,则通过 map 调用对应实体类的 fromJson
方法进行转换。同样的如果创建了多个实体类,这里也会存在多个类似的 if 判断语句。
所以最终其实是调用实体类的 fromJson
方法,而该方法则调用的是 xxxx_entity.g.dart
里生成的 $UserEntityFromJson
方法。
整体流程如下:
1.5 json_field.dart
包含 JsonSerializable
和 JSONField
两个注解。存放目录为 generated/json/base
1 | class JsonSerializable{ |
- JsonSerializable 类注解,二次生成代码时插件查找该注解的类进行生成。
- JSONField 字段注解,用于自定义字段映射和配置是否序列化和反序列化字段
2. 使用
2.1 单实体解析
直接调用实体类对应的 fromJson
方法即可将 Json 数据解析为实体对象。
1 | String userData = """ |
fromJson 需要的参数是 Map ,所以需要先使用 jsonDecode 将 Json 字符串转换为 Map
除了直接使用实体类的 fromJson
方法外也可以直接使用生成的 JsonConvert
来解析:
1 | String userData = """ |
使用 JsonConvert
的 convert
、 asT
、fromJsonAsT
方法可以实现相同的解析效果。
2.2 List 解析
解析 Json List 数据则需要调用 JsonConvert
的对应方法进行解析,除了使用上面的 convert
、asT
、fromJsonAsT
外,还可以使用 convertList
、convertListNotNull
:
1 | String 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
重新生成代码:
1 | String? id; |
添加 @JSONField 注解后一定要使用
Alt
+J
重新生成代码,否则不生效
1 | String userData = """ |
这样就能实现 AGE 与 age 字段的映射。
忽略字段
JSONField 还有两个字段 serialize
、 deserialize
用于序列化和反序列化时忽略某个字段,比如不需要解析 name 字段则可设置 deserialize
为 false ,如果 toJson 时不需要序列化某个字段,则设置 serialize
为 false。
1 | false) (deserialize: |
当字段设置 @JSONField(deserialize: false)
时即使 Json 数据有该字段也不会进行解析,打印字段值为 null ,同样的如果设置 @JSONField(serialize: false)
时,当调用 toJson
时,即使字段有值转换为 Json 数据也不会有该字段。
3. 优化
上面已经讲解了使用插件生成实体类后如何进行 Json 数据解析的基本使用,但是在实际项目开发过程中会存在一定的问题,实际项目开发中接口返回的数据格式一般是这样的:
1 | { |
在返回数据外又统一包裹了一层,data 字段的数据才是实际业务需要的数据,而不同的接口返回的 data 数据结构也不相同,如果直接使用插件生成的,会生成如下代码:
1 | () |
这样的话每一个接口都要生成一个 ResponseEntity 类,使用起来也不方便不便于统一封装。所以需要对 ResponseEntity 进行改造,让其支持泛型解析。
首先重新使用上面的 Json 示例数据生成一个 ApiResponseEntity
,然后将 data 字段类型改为 dynamic
,使用 Alt
+ J
重新生成代码:
1 | () |
再将 @JsonSerializable()
注解去掉,把 api_response_entity.dart
和 api_response_entity.g.dart
文件放到一个单独的文件夹内
前面说了使用
Alt
+J
重新生成代码会根据@JsonSerializable()
注解生成,因为需要修改ApiResponseEntity
类来满足泛型解析的需求,所以要去除@JsonSerializable()
注解防止重新生成代码将自定义代码覆盖掉。而去掉了@JsonSerializable()
注解后,下次生成代码时会自动删除generated/json
下多余的.g.dart
,所以需要将其拷贝到其他目录防止下次生成时被删除。
最后给 ApiResponseEntity
以及 ApiResponseEntityFromJson
添加泛型支持。修改后内容如下:
1 | class ApiResponseEntity<T> { |
给 ApiResponseEntity
上加上泛型 T
,然后修改 data 类型为 T?
, 再给 $ApiResponseEntityFromJson
方法上添加泛型,解析 data 数据的时候就可以直接使用 jsonConvert.convert<T>
进行解析。
修改完后使用示例如下:
- 单实体解析
1 | String userData = """ |
- List 解析
1 | String userData = """ |
- 基本数据类型解析
1 | String jsonData = """ |
经过上面的改造以后,ApiResponseEntity
则满足项目开发中使用。
FlutterJsonBeanFactory 插件源码:FlutterJsonBeanFactory
审阅:@七彩祥云至尊宝
Flutter 应用框架搭建系列文章: