Flutter之Json数据解析

2022-09-27 10:46:56 浏览数 (2)

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 如下:

代码语言:javascript复制
@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 两个方法,以 $  实体类名 为前缀,生成内容如下:

代码语言:javascript复制
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.dartJsonConvert 类, 用于统一进行 Json 与实体类的转换,存放目录为 generated/json/base, 生成内容如下:

代码语言:javascript复制
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 数据转换为实体对象,源码如下:

代码语言:javascript复制
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, 源码如下:

代码语言:javascript复制
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

convertListNotNullconvertList 作用相同,也是将 Json 数据转换为实体 List ,源码如下:

代码语言:javascript复制
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 方法时 convertListNotNullasT 后面加了一个 ! ,表示不为空。

当在实体类里定义字段为 List 类型时,会根据是否为非空类型而选择生成 convertListconvertListNotNull 来进行转换:

List<Xxxx?>? : 当定义 List 为可空类型,且 List 里元素的类型也为可空类型时,使用 convertListList<Xxxx>? : 当定义 List 为可空类型,但 List 里元素的类型为非空类型时,使用 convertListNotNullList<Xxxx>? : 当定义 List 为非空类型,且 List 里元素的类型也为非空类型时,使用 convertListNotNull

asT

convertconvertListconvertListNotNull 最终调用的都是 asT 方法,源码如下:

代码语言:javascript复制
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() 获取泛型类型的名称,再与 StringintdoubleDateTimebool 这些基础数据类型进行比较,如果是这些类型则调用这些类型的转换方法进行转换。

最后,如果不是基础类型则调用 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 为单个实体对象的转换,源码如下:

代码语言:javascript复制
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 数据,源码如下:

代码语言:javascript复制
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

包含 JsonSerializableJSONField 两个注解。存放目录为 generated/json/base

代码语言:javascript复制
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 数据解析为实体对象。

代码语言:javascript复制
String userData = """
     {
        "id":"12313",
        "name":"loongwind",
        "age":18
     }
    """;
UserEntity user = UserEntity.fromJson(jsonDecode(userData));

fromJson 需要的参数是 Map ,所以需要先使用 jsonDecode 将 Json 字符串转换为 Map

除了直接使用实体类的 fromJson 方法外也可以直接使用生成的 JsonConvert 来解析:

代码语言:javascript复制
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));

使用 JsonConvertconvertasTfromJsonAsT 方法可以实现相同的解析效果。

2.2 List 解析

解析 Json List 数据则需要调用 JsonConvert 的对应方法进行解析,除了使用上面的 convert asTfromJsonAsT 外,还可以使用 convertListconvertListNotNull

代码语言:javascript复制
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));

convertListconvertListNotNullconvert asTfromJsonAsT 的区别在于前者的泛型为 List Item元素的泛型类型,后者则直接为对应 List 的类型。如上面 convertListconvertListNotNull 的泛型直接为 UserEntity , 而 convert asTfromJsonAsT 的泛型为 List<UserEntity>

2.3 JSONField 的使用

自定义字段名

实际开发中可能会存在 Json 数据字段与代码中的字段不一致的情况,比如 Json 中的字段命名不符合代码规范,这个时候就可以使用 JSONField 来实现自定义的字段映射。

如 Json 里的字段为 AGE 需要映射到实体类的 age 字段,只需要在实体类的 age 字段上加上 JSONField 注解,指定 name 为 AGE , 然后使用 Alt J 重新生成代码:

代码语言:javascript复制
  String? id;
   String? name;
   @JSONField(name: "AGE")
   int? age;

添加 @JSONField 注解后一定要使用 Alt J 重新生成代码,否则不生效

代码语言:javascript复制
String userData = """
{
  "id":"12313",
  "name":"loongwind",
  "AGE":18
}
""";

UserEntity user = UserEntity.fromJson(jsonDecode(userData));
print(user.age) // 18

这样就能实现 AGE 与 age 字段的映射。

忽略字段

JSONField 还有两个字段 serializedeserialize 用于序列化和反序列化时忽略某个字段,比如不需要解析 name 字段则可设置 deserialize 为 false ,如果 toJson 时不需要序列化某个字段,则设置 serialize 为 false。

代码语言:javascript复制
@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 重新生成代码:

代码语言:javascript复制
@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.dartapi_response_entity.g.dart 文件放到一个单独的文件夹内

前面说了使用 Alt J 重新生成代码会根据 @JsonSerializable() 注解生成,因为需要修改 ApiResponseEntity 类来满足泛型解析的需求,所以要去除 @JsonSerializable() 注解防止重新生成代码将自定义代码覆盖掉。而去掉了 @JsonSerializable() 注解后,下次生成代码时会自动删除 generated/json 下多余的 .g.dart,所以需要将其拷贝到其他目录防止下次生成时被删除。

最后给 ApiResponseEntity 以及 ApiResponseEntityFromJson 添加泛型支持。修改后内容如下:

代码语言:javascript复制
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 则满足项目开发中使用。

0 人点赞