Flutter实体序列化插件entity_serializer的使用

Flutter实体序列化插件entity_serializer的使用

entity_serializer 是一个用于简化数据类和序列化器构建过程的Flutter插件。该插件通过XML文件描述模型,生成相应的Dart代码,从而实现数据类的序列化和反序列化。

功能简介

  • Pub Package: Pub Package
  • GitHub Issues: GitHub Issues
  • GitHub Forks: GitHub Forks
  • GitHub Stars: GitHub Stars
  • GitHub License: GitHub License

解决的问题

在开发过程中,经常会遇到需要创建多个数据类(如客户端与服务器之间的JSON通信数据类,以及数据库存储的数据类)。使用 entity_serializer 可以减少重复的代码编写,提高开发效率。

如何工作

所有数据类应以简单的XML文件形式描述。在该文件中,定义了一个或多个可以操作数据类的序列化器。然后运行构建器或命令行工具生成 .dart 文件,这些文件可以在项目中使用。从这时起,就可以使用专用的序列化器对数据类进行序列化/反序列化。例如,对于 Foo 类,可以直接将其序列化为 Map<String, dynamic> 用于JSON和数据库存储,两种情况下可以有不同的序列化规则。整个过程无需中间DTO类,仅需扩展 List<dynamic>Map<String, dynamic> 以及数据类即可。

限制

当前版本(及未来版本)不支持嵌套集合。例如,无法处理包含映射的列表或映射的映射等。如果需要此类功能,请将映射包装在一个自定义类型中。此外,该包将在满足所有需求前持续开发和扩展。

CLI 版本

存在 cli_main.dart 文件,它允许编译并使此工具成为独立应用,而不依赖于 build_runner。要使用它,请调用带有 -h 参数来获取使用说明。通常情况下,你会调用:

cli_main -i data_model/list_test.xml -o sources/gen.dart

或者

cli_main -s -i data_model/list_test.xml -o sources/

XML 描述

有两种方法来描述模型:

  1. 所有内容都在一个文件中,使用 spec 根节点。
  2. 序列化器和实体分布在多个XML文件中。

多个XML文件(组合)

如果想将模型拆分为多个XML文件,可以按照以下步骤操作:

  1. 定义主XML文件,指向所有组件XML文件。
  2. 定义包含序列化器定义的XML文件。
  3. 定义包含实体定义的XML文件。

主XML文件

<composition>
    <include file="comp_serializer.xml"/>
    <include file="comp_entities_1.xml"/>
    <include file="comp_entities_2.xml"/>
</composition>

序列化XML文件

根节点必须命名为 serializers,且只能包含 serializerserializerTemplate 节点。

实体XML文件

根节点必须命名为 entities,且只能包含 classproxyimport 节点。

单一XML文件

以下是用于生成一切的XML格式的简要描述。我们来看一些例子和说明:

<?xml version="1.0"?>
<spec>
    <import package="package:any_package.dart"/>
    <serializer name="Ser1"/>

    <class name="Book">
        <string name="name"/>
    </class>
</spec>

这是最简单的有意义的XML。我们定义了一个名为 Ser1 的序列化器和一个名为 Book 的数据类。预期 Book 类只包含一个名为 nameString 字段。为 Book 类生成序列化扩展。

导入节点

如果需要向生成的代码添加额外导入,可以使用 import 节点。该节点有一个强制属性 package,用于指定需要导入的内容。每个XML可以有多个 import 节点。

序列化模板节点

serializerTemplate 节点几乎与 serializer 节点相同,主要区别在于构建器不会为 serializerTemplate 生成任何代码。它只是一个模板,供其他序列化器继承。每个序列化器可以通过 inheritFrom 属性继承 serializerTemplate 中的特殊化。

序列化节点

serializer 节点告知构建器需要为数据类生成命名序列化器。当前序列化器具有以下属性:

  • name - 必须的,该名称作为代码生成的前缀,并且也可以放在 class 节点的 serializers 属性中。
  • inheritFrom - 可选,默认为空。逗号分隔的 serializerTemplate 名称,从中复制特殊化。

默认情况下,序列化器可以处理 Dart 类型:intdoubleStringbool 以及在XML中描述的所有实体类。要进一步处理其他类的编码,可以通过序列化的子节点进行。详见下一节。

别名

每个类型的每种特殊化都可以有字段别名。为了在序列化/反序列化字段时使用新名称,只需添加以字段名称前缀 _ 开头的属性,值是新名称。例如:

<spec>
    <serializer name="KeepDate" >
        <keep type="DateTime"/>
        <keep type="int" _justInt="i"/>
    </serializer>

    <class name="TestEntity" copyWith="false">
        <DateTime name="date"/>
        <DateTime name="noAlias"/>
        <int name="justInt"/>
    </class>
</spec>

结果序列化器 KeepDate 对于类型 int 将所有名为 justInt 的字段重新映射为 i

保持

序列化器使用某些规则将 Dart 对象实例转换为 Map<String, dynamic>。然而,有时你希望保留给定对象实例不变。因此,你想将对象直接复制到映射中。为此,存在 keep 节点,它仅包含一个 type 属性。如果序列化器找到给定类型的节点,则会直接复制它。例如:

<serializer name="KeepDate" >
    <keep type="DateTime"/>
</serializer>
特殊化

对于希望以特殊方式处理的类型,应该使用 specialization 节点。以下是其外观:

<serializer name="EpocDate">
    <specialization
        type="DateTime"
        outType="int"
        serialization="dateTimeToEpoc"
        deserialization="dateTimeFromEpoc"
        import="package:entity_serializer/entity_serializer.dart"
    />
</serializer>

这里是对属性的描述:

  • type - 这条规则适用的类型。
  • outType - serialization 属性所指向函数的结果。
  • serialization - 应该使用的函数名称,用于将 type 转换为 outType
  • deserialization - 应该使用的函数名称,用于将 outType 转换为 type
  • import - 函数可以从哪个包/源文件导入,这是一个可选属性。

类节点

每个需要生成的数据类都必须放入XML中的 class 节点内。以下属性可用于此节点:

  • name - 必须的,告诉新类的名称,应该是PascalCase风格。
  • copyWith - 可选,默认为 true。告诉新创建的类是否应具有 @CopyWith 注解。
  • serializers - 可选,默认为 null。应为此类生成的所有序列化器的列表。如果为空/空,则使用所有已知序列化器。
  • generateEntity - 可选,默认为 true。告诉构建器是否应生成Dart类。如果是 false,则仅生成序列化器。需要手动导入类体。
  • apiProxy - 可选,默认为空。应为此实体生成的API代理的逗号分隔列表。参见 proxy 节点文档了解更多信息。
  • mixin - 可选,默认为空。应添加到类定义中的混合的逗号分隔列表。

要为新创建的类创建字段,可以添加子节点。每个子节点支持以下属性:

  • name - 必须的,告诉字段应如何命名,适用于Dart语言的命名限制。
  • final - 可选,默认为 true。如果设置为 true,则使类字段为 final
  • comment - 可选,默认为 null。如果设置,可以包括字段的注释。

示例:

<?xml version="1.0"?>
<spec>
    <class name="Book">
        <string name="name"/>
        <string name="author" final="false"/>
    </class>
</spec>

结果为:

@CopyWith()
class Book {
  final String name;
  String author;

  MyType({
    required this.name,
    required this.author,
  });
}

类子节点:普通Dart类型

以下是为普通Dart类型保留的子节点:intdoubleboolString

示例:

<?xml version="1.0"?>
<spec>
    <class name="Book">
        <string name="str1"/>
        <optionalString name="str2"/>
        <int name="int1"/>
        <optionalInt name="int2"/>
        <double name="dbl1"/>
        <optionalDouble name="dbl2"/>
        <bool name="bool1"/>
        <optionalBool name="bool2"/>
    </class>
</spec>

类子节点:集合 - 列表

对于Dart List 类型,需要使用专门的节点,它具有上述所有属性外加必需的 innerType 属性。示例:

<class name="MyList">
    <list name="integers" innerType="int"/>
    <list name="customType" innerType="MyType"/>
    <list name="dynamicType" innerType="dynamic"/>
    <list name="expDynamicType" innerType="dynamic" expectOnly="MyType,MyOther"/>
</class>

值得注意的是,innerType 可以指向 dynamic,这将生成额外代码以处理此XML中所有数据类型的类。如果要限制动态列表中的类检测,则 expectOnly 属性发挥作用。你可以指定仅处理那些类型。

类子节点:集合 - 映射

对于Dart Map 类型,需要使用专门的节点,它具有上述所有属性外加两个必需的 keyTypevalueType 属性。valueType 的行为与 list 节点中的 innerType 相同。简单示例:

<class name="MyMap">
    <map name="integers" keyType="String" valueType="int"/>
    <map name="strings" keyType="String" valueType="String"/>
    <map name="customType" keyType="String" valueType="MyType"/>
    <map name="dynamicType" keyType="String" valueType="dynamic"/>
    <map name="expDynamicType" keyType="String" valueType="dynamic" expectOnly="MyType,MyOther"/>
</class>

类子节点:字段

class 节点中最通用的子节点是 field,它允许你在类中添加任意类型的变量。它有两个必需的属性 nametype。示例:

<class name="Book" copyWith="false">
    <field name="author" type="Author"/>
    <field name="meta" type="Meta?"/>
    <optionalField name="extra" type="String"/>
</class>

类子节点:任何名称

如果你想在其他类中使用自定义数据类作为变量类型,只需输入其名称。请看这个小示例:

<spec>
    <class name="Book" copyWith="false">
        <field name="author" type="Author"/>
        <field name="meta" type="Meta"/>
        <Meta name="anotherMeta"/>
        <meta name="andAnotherMeta"/>
    </class>

    <class name="Author" copyWith="false">
        <string name="name"/>
        <string name="surname"/>
    </class>

    <class name="Meta" copyWith="false">
        <String name="country"/>
    </class>
</spec>

代理节点

当需要满足其他包(如 retrofit)的要求时,代理进入游戏。这一特性允许生成其他包期望的方法。目前支持以下第三方库:

  • json_serializable

要启用API代理,需在文件中添加以下XML节点:

<proxy name="myProxy" type="json_serializable" serializer="MyJson"/>

其中:

  • name - 必须的,你的代理名称。该名称需要在 class 节点的 apiProxy 属性中给出。
  • type - 必须的,此包支持的预定义类型。
  • serializer - 必须的,应包装在代理生成方法中的序列化器名称。

添加 proxy 节点后,可以向任何类节点添加 apiProxy 属性,示例:

<spec>
    <serializer name="MyJson">
        <specialization
            type="DateTime"
            serialization="dateTimeToIsoStr"
            deserialization="dateTimeFromIsoStr"
            import="package:entity_serializer/entity_serializer.dart"
        />
    </serializer>

    <proxy name="myProxy" type="json_serializable" serializer="MyJson"/>

    <class name="TestEntity" apiProxy="myProxy">
        <DateTime name="date"/>
        <int name="justInt"/>
    </class>
</spec>

结果将生成 TestEntity 类的方法 factory TestEntity.fromJson(Map<String, dynamic> json)Map<String, dynamic> toJson()

示例代码

import 'dart:convert';

import 'package:example/src/model/pure_dynamic_test.dart';

import 'src/model/data_composition_test.dart';
import 'src/model/date_test.dart';
import 'src/model/list_test.dart';
import 'src/model/map_test.dart';

void checkListGen() {
  print("####### checkListGen\n");
  MyList myList = MyList(integers: [
    1,
    2,
    3
  ], strings: [
    "a",
    "b"
  ], customType: [
    MyTypeInList(val: 6),
    MyTypeInList(val: 3)
  ], dynamicType: [
    1,
    2.4,
    "a",
    MyTypeInList(val: 6),
    MyOtherInList(str: "str")
  ], expDynamicType: [
    MyTypeInList(val: 36),
    MyOtherInList(str: "str33")
  ]);
  JsonEncoder encoder = JsonEncoder.withIndent('  ');
  String prettyprint = encoder.convert(myList.toSer1());
  print(prettyprint);

  Map<String, dynamic> dec = jsonDecode(prettyprint);
  final result = dec.toMyListUsingSer1();
  print(result);
  print("\n\n");
}

void checkCompositeGen() {
  print("####### checkCompositeGen\n");
  Book book = Book(
    author: Author(name: "name", surname: "surname"),
    meta: Meta(
      country: Country(code: "code", extra: "extra"),
    ),
    pages: 10,
    superMeta: SuperMeta(version: 3),
  );
  JsonEncoder encoder = JsonEncoder.withIndent('  ');
  final maps = book.toSer1();
  String prettyprint = encoder.convert(maps);
  print(prettyprint);

  Map<String, dynamic> dec = jsonDecode(prettyprint);
  final result = dec.toBookUsingSer1();
  print(result);
  print("\n\n");
}

void checkMapGen() {
  print("####### checkMapGen\n");
  MyMap map = MyMap(
    integers: {"a": 1, "b": 2, "c": 3},
    strings: {"a": "1", "b": "2", "c": "3"},
    customType: {"a": MyType(val: 6), "b": MyType(val: 3)},
    dynamicType: {
      "a": 1,
      "b": 2.4,
      "c": "str",
      "d": MyType(val: 6),
      "e": MyOther(str: "str")
    },
    expDynamicType: {"a": MyType(val: 36), "b": MyOther(str: "str33")},
  );

  JsonEncoder encoder = JsonEncoder.withIndent('  ');
  final maps = map.toSer1();
  String prettyprint = encoder.convert(maps);
  print(prettyprint);

  Map<String, dynamic> dec = jsonDecode(prettyprint);
  final result = dec.toMyMapUsingSer1();
  print(result);
  print("\n\n");
}

void checkDateGen() {
  print("####### checkDateGen\n");
  TestEntity entity = TestEntity(date: DateTime(1999, 1, 2, 12, 30, 55));
  JsonEncoder encoder = JsonEncoder.withIndent('  ');
  final maps = entity.toIsoDate();
  String prettyprint = encoder.convert(maps);
  print(prettyprint);

  Map<String, dynamic> dec = jsonDecode(prettyprint);
  final result = dec.toTestEntityUsingIsoDate();
  print(result.date);
  print("\n\n");
}

void checkPureEvil() {
  EvilDynamic evilDynamic = EvilDynamic(
    evil: EvilTwo(justString: "s"),
    limitedEvil: EvilOne(justInt: 1),
  );
  final maps = evilDynamic.toSer();

  final dec = maps.toEvilDynamicUsingSer();
  print(dec.evil.justString);
  print(dec.limitedEvil.justInt);
}

void main() {
  checkListGen();
  checkCompositeGen();
  checkMapGen();
  checkDateGen();
  checkPureEvil();
}

更多关于Flutter实体序列化插件entity_serializer的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter实体序列化插件entity_serializer的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


entity_serializer 是一个用于 Flutter 的实体序列化插件,它可以帮助开发者将 Dart 对象(实体类)序列化为 JSON 格式,或者将 JSON 格式反序列化为 Dart 对象。这个插件的主要目的是简化实体类的序列化和反序列化过程,减少样板代码的编写。

安装 entity_serializer

首先,你需要在 pubspec.yaml 文件中添加 entity_serializer 依赖:

dependencies:
  flutter:
    sdk: flutter
  entity_serializer: ^1.0.0  # 请使用最新版本

然后运行 flutter pub get 来安装依赖。

使用 entity_serializer

1. 定义实体类

假设我们有一个 User 实体类,包含 idnameemail 三个属性:

class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});
}

2. 生成序列化代码

entity_serializer 提供了一个注解 [@Entity](/user/Entity),你可以使用这个注解来标记需要序列化的实体类。然后,entity_serializer 会自动生成序列化和反序列化的代码。

import 'package:entity_serializer/entity_serializer.dart';

[@Entity](/user/Entity)()
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});
}

在终端中运行以下命令来生成序列化代码:

flutter packages pub run build_runner build

这将会生成一个 user.g.dart 文件,其中包含 User 类的序列化和反序列化代码。

3. 使用生成的代码

生成的代码中会包含 toJsonfromJson 方法,你可以使用这些方法来序列化和反序列化 User 对象。

import 'user.g.dart';

void main() {
  // 创建一个 User 对象
  User user = User(id: 1, name: "John Doe", email: "john.doe@example.com");

  // 序列化为 JSON
  Map<String, dynamic> json = user.toJson();
  print(json);  // 输出: {id: 1, name: John Doe, email: john.doe@example.com}

  // 反序列化为 User 对象
  User userFromJson = User.fromJson(json);
  print(userFromJson.name);  // 输出: John Doe
}

自定义序列化行为

如果你需要自定义某些字段的序列化行为,可以在字段上使用 [@SerializedName](/user/SerializedName) 注解来指定 JSON 中的字段名,或者使用 [@SerializableField](/user/SerializableField) 注解来指定自定义的序列化和反序列化逻辑。

[@Entity](/user/Entity)()
class User {
  [@SerializedName](/user/SerializedName)("user_id")
  final int id;

  final String name;

  [@SerializableField](/user/SerializableField)(
    toJson: (email) => email.toString().toLowerCase(),
    fromJson: (json) => json.toString(),
  )
  final String email;

  User({required this.id, required this.name, required this.email});
}
回到顶部