JSON 和序列化
如何在 Flutter 中使用 JSON。
很难想象一个不需要与 Web 服务器通信或以某种方式轻松存储结构化数据的移动应用程序。在构建联网应用程序时,迟早需要使用一些经典的 JSON 格式。
本指南探讨了在 Flutter 中使用 JSON 的方法。它涵盖了在不同场景下使用哪种 JSON 解决方案以及原因。
哪种 JSON 序列化方法适合我?
#本文涵盖了两种通用的 JSON 处理策略
- 手动序列化
- 使用代码生成进行自动序列化
不同的项目具有不同的复杂性和用例。对于较小的概念验证项目或快速原型,使用代码生成器可能过于繁琐。对于具有多个 JSON 模型且复杂度较高的应用程序,手动编码会很快变得繁琐、重复且容易出现许多小错误。
对于小型项目,使用手动序列化
#手动 JSON 解码是指使用 dart:convert 中的内置 JSON 解码器。它涉及将原始 JSON 字符串传递给 jsonDecode() 函数,然后在生成的 Map<String, dynamic> 中查找所需的值。它没有外部依赖项或特定的设置过程,并且适用于快速的概念验证。
当您的项目变得更大时,手动解码的性能不会很好。手动编写解码逻辑会变得难以管理且容易出错。如果您在访问不存在的 JSON 字段时出现拼写错误,您的代码将在运行时抛出错误。
如果您在项目中没有很多 JSON 模型并且希望快速测试概念,手动序列化可能是您想要开始的方式。有关手动编码的示例,请参阅 使用 dart:convert 手动序列化 JSON。
对于中大型项目,使用代码生成
#使用代码生成进行 JSON 序列化意味着让外部库为您生成编码样板代码。在进行一些初始设置后,您运行一个文件监视器,该监视器从您的模型类中生成代码。例如,json_serializable 和 built_value 是这些类型的库。
这种方法可以很好地扩展到更大的项目。无需手动编写样板代码,并且访问 JSON 字段时的拼写错误会在编译时捕获。代码生成的缺点是它需要一些初始设置。此外,生成的源代码可能会在您的项目导航器中产生视觉混乱。
如果您有一个中型或大型项目,则可能需要使用生成的代码进行 JSON 序列化。有关基于代码生成 JSON 编码的示例,请参阅 使用代码生成库序列化 JSON。
Flutter 中是否有 GSON/Jackson/Moshi 的等效替代方案?
#
简单的答案是:没有。
这样的库需要使用运行时 反射,这在 Flutter 中被禁用。运行时反射会干扰 树摇,而 Dart 已经支持树摇很长时间了。通过树摇,您可以“摇掉”发布版本中的未使用的代码。这可以显著优化应用程序的大小。
由于反射默认使所有代码都处于使用状态,因此使得树摇变得困难。工具无法知道运行时哪些部分未被使用,因此很难删除冗余代码。使用反射时,应用程序大小无法轻松优化。
虽然您无法在 Flutter 中使用运行时反射,但有些库为您提供类似易于使用的 API,但它们基于代码生成。这种方法将在 代码生成库 部分中更详细地介绍。
使用 dart:convert 手动序列化 JSON
#Flutter 中的基本 JSON 序列化非常简单。Flutter 具有内置的 dart:convert 库,其中包含一个简单的 JSON 编码器和解码器。
以下示例 JSON 实现了一个简单的用户模型。
{
"name": "John Smith",
"email": "john@example.com"
}
使用 dart:convert,您可以以两种方式序列化此 JSON 模型。
内联序列化 JSON
#通过查看 dart:convert 文档,您会看到可以通过调用 jsonDecode() 函数(JSON 字符串作为方法参数)来解码 JSON。
final user = jsonDecode(jsonString) as Map<String, dynamic>;
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
不幸的是,jsonDecode() 返回一个 dynamic,这意味着您不知道值的类型,直到运行时。使用这种方法,您将失去大部分静态类型语言的功能:类型安全、自动完成,最重要的是,编译时异常。您的代码会立即变得更容易出错。
例如,每当您访问 name 或 email 字段时,您可能会快速引入拼写错误。编译器不知道的拼写错误,因为 JSON 存在于 map 结构中。
在模型类中序列化 JSON
#通过引入一个普通模型类(在本例中称为 User)来解决上述问题。在 User 类中,您会找到
- 一个
User.fromJson()构造函数,用于从 map 结构构造一个新的User实例。 - 一个
toJson()方法,它将User实例转换为 map。
使用这种方法,调用代码可以具有类型安全、name 和 email 字段的自动完成以及编译时异常。如果您犯了拼写错误或将字段视为 int 而不是 String,应用程序将不会编译,而是不会在运行时崩溃。
user.dart
class User {
final String name;
final String email;
User(this.name, this.email);
User.fromJson(Map<String, dynamic> json)
: name = json['name'] as String,
email = json['email'] as String;
Map<String, dynamic> toJson() => {'name': name, 'email': email};
}
解码逻辑的责任现在转移到模型本身。使用这种新方法,您可以轻松解码用户。
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
要编码用户,请将 User 对象传递给 jsonEncode() 函数。您不需要调用 toJson() 方法,因为 jsonEncode() 已经为您完成了此操作。
String json = jsonEncode(user);
使用这种方法,调用代码无需担心 JSON 序列化。但是,模型类仍然必须这样做。在生产应用程序中,您需要确保序列化正常工作。实际上,User.fromJson() 和 User.toJson() 方法都需要有单元测试来验证正确的行为。
但是,现实世界的场景并不总是那么简单。有时 JSON API 响应更复杂,例如,因为它们包含嵌套的 JSON 对象,这些对象必须通过自己的模型类进行解析。
如果有一种方法可以为您处理 JSON 编码和解码,那就太好了!幸运的是,有!
使用代码生成库序列化 JSON
#虽然还有其他库可用,但本指南使用 json_serializable,这是一个自动源代码生成器,可为您生成 JSON 序列化样板代码。
由于序列化代码不再手动编写或手动维护,因此您最大限度地降低了在运行时出现 JSON 序列化异常的风险。
在项目中设置 json_serializable
#要在您的项目中包含 json_serializable,您需要一个常规依赖项和两个开发依赖项。简而言之,开发依赖项是不包含在我们的应用程序源代码中的依赖项——它们仅在开发环境中使用的依赖项。
要添加依赖项,请运行 flutter pub add
flutter pub add json_annotation dev:build_runner dev:json_serializable
在您的项目根文件夹中运行 flutter pub get(或在您的编辑器中单击Packages get),以使这些新依赖项在您的项目中可用。
以 json_serializable 的方式创建模型类
#以下显示了如何将 User 类转换为 json_serializable 类。为了简单起见,此代码使用来自先前示例的简化 JSON 模型。
user.dart
import 'package:json_annotation/json_annotation.dart';
/// This allows the `User` class to access private members in
/// the generated file. The value for this is *.g.dart, where
/// the star denotes the source file name.
part 'user.g.dart';
/// An annotation for the code generator to know that this class needs the
/// JSON serialization logic to be generated.
@JsonSerializable()
class User {
User(this.name, this.email);
String name;
String email;
/// A necessary factory constructor for creating a new User instance
/// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
/// The constructor is named after the source class, in this case, User.
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
/// `toJson` is the convention for a class to declare support for serialization
/// to JSON. The implementation simply calls the private, generated
/// helper method `_$UserToJson`.
Map<String, dynamic> toJson() => _$UserToJson(this);
}
使用此设置,源代码生成器会生成用于编码和解码 name 和 email 字段的代码。
如果需要,也可以轻松自定义命名策略。例如,如果 API 返回带有snake_case的对象,并且您想在模型中使用lowerCamelCase,则可以使用带有 name 参数的 @JsonKey 注释
/// Tell json_serializable that "registration_date_millis" should be
/// mapped to this property.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
最好是服务器和客户端都遵循相同的命名策略。@JsonSerializable() 提供 fieldRename 枚举,用于完全将 dart 字段转换为 JSON 键。
修改 @JsonSerializable(fieldRename: FieldRename.snake) 等效于将 @JsonKey(name: '<snake_case>') 添加到每个字段。
有时服务器数据不确定,因此有必要在客户端验证和保护数据。其他常用的 @JsonKey 注解包括
/// Tell json_serializable to use "defaultValue" if the JSON doesn't
/// contain this key or if the value is `null`.
@JsonKey(defaultValue: false)
final bool isAdult;
/// When `true` tell json_serializable that JSON must contain the key,
/// If the key doesn't exist, an exception is thrown.
@JsonKey(required: true)
final String id;
/// When `true` tell json_serializable that generated code should
/// ignore this field completely.
@JsonKey(ignore: true)
final String verificationCode;
运行代码生成工具
#首次创建 json_serializable 类时,您会遇到类似以下的错误
Target of URI hasn't been generated: 'user.g.dart'.
这些错误完全正常,仅仅是因为模型类的生成代码尚未存在。要解决此问题,请运行代码生成器以生成序列化样板代码。
有两种方法可以运行代码生成器。
一次性代码生成
#通过在项目根目录下运行 dart run build_runner build --delete-conflicting-outputs,您可以在需要时为模型生成 JSON 序列化代码。这将触发一次性构建,它会遍历源代码文件,选择相关文件,并为它们生成必要的序列化代码。
虽然这很方便,但如果您每次更改模型类时都不需要手动运行构建,那就更好了。
持续生成代码
#一个观察者使我们的源代码生成过程更加方便。它会监视项目文件的更改,并在需要时自动构建必要的文件。通过在项目根目录下运行 dart run build_runner watch --delete-conflicting-outputs 来启动观察者。
可以安全地启动观察者并将其在后台运行。
使用 json_serializable 模型
#要以 json_serializable 的方式解码 JSON 字符串,您实际上不需要对我们之前的代码进行任何更改。
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);
编码也是如此。调用的 API 与之前相同。
String json = jsonEncode(user);
使用 json_serializable 后,您可以忘记在 User 类中进行任何手动 JSON 序列化。源代码生成器会创建一个名为 user.g.dart 的文件,其中包含所有必要的序列化逻辑。您不再需要编写自动化测试来确保序列化正常工作——现在库的责任是确保序列化能够适当地工作。
生成嵌套类的代码
#您可能有一些包含类内嵌套类的代码。如果是这样,并且您尝试将该类作为参数传递给服务(例如 Firebase),您可能会遇到 Invalid argument 错误。
考虑以下 Address 类
import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';
@JsonSerializable()
class Address {
String street;
String city;
Address(this.street, this.city);
factory Address.fromJson(Map<String, dynamic> json) =>
_$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
Address 类嵌套在 User 类中
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User(this.name, this.address);
String name;
Address address;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
在终端中运行 dart run build_runner build --delete-conflicting-outputs 会创建 *.g.dart 文件,但私有的 _$UserToJson() 函数看起来如下
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'name': instance.name,
'address': instance.address,
};
现在一切看起来都很好,但是如果您对 user 对象执行 print()
Address address = Address('My st.', 'New York');
User user = User('John', address);
print(user.toJson());
结果是
{name: John, address: Instance of 'address'}
而您可能想要输出如下所示
{name: John, address: {street: My st., city: New York}}
要使其工作,请在类声明上方的 @JsonSerializable() 注解中传递 explicitToJson: true。现在 User 类如下所示
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable(explicitToJson: true)
class User {
User(this.name, this.address);
String name;
Address address;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
有关更多信息,请参阅 explicitToJson 在 JsonSerializable 类中的说明,以及 json_annotation 包的说明。
进一步参考
#有关更多信息,请参阅以下资源
dart:convert和JsonCodec文档- pub.dev 上的
json_serializable包 - GitHub 上的
json_serializable示例 - 深入了解 Dart 的模式和记录的 codelab
- 关于 如何在 Dart/Flutter 中解析 JSON 的终极指南