JSON 和序列化数据
很难想象一个移动应用会不需要与 web 服务器通信或者在某些时候轻松地存储结构化数据。当创造需要网络连接的应用时,它可能迟早会处理一些旧的 JSON。
本指南介绍了如何在 Flutter 中使用 JSON。包括了如何在不同场景中使用相应的 JSON 解决方案以及为什么要这么做。
术语:**编码 和 序列化数据 是一回事 - 将数据结构转换为字符串。 解码 和 反序列化数据 则是相反的过程 - 将字符串转换为数据结构。然而,序列化数据** 通常也指将数据结构转换为更加易读的数据格式的整个过程。
为了避免混淆,本文档在涉及到整个过程时使用“序列化数据”,在特指这些过程时使用“编码”和“解码”。
我需要哪一种 JSON 序列化数据方法?
本文涵盖了两种常规的 JSON 使用策略:
手动序列化数据
利用代码生成进行自动序列化数据
不同的项目复杂度不同,用例也不一样。对于较小的概念验证项目或者快速原型,使用代码生成器可能是过度的。对于具有很多更加复杂的 JSON 模型的 App,手动编码可能很快变得无聊,重复并且发生很多小错误。
为较小的项目使用手动序列化数据
手动 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 支持了相当久的 tree shaking。通过 tree shaking,你可以从你的发布版本中“抖掉”不需要使用的代码。这会显著优化 App 的体积。
由于反射会默认让所有的代码被隐式使用,这让 tree shaking 变得困难。工具不知道哪一部分在运行时不会被用到,所以冗余的代码很难被清除。当使用反射时,App 的体积不能被轻易优化。
dartson 怎么样? [dartson][dartson] 是一个使用运行时 反射) 的库,这让它不能兼容 Flutter。
尽管你不能在 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 文档,你会看到你可以将 JSON 字符串作为方法的参数来调用jsonDecode()
方法来解码 JSON。
Map<String, dynamic> user = jsonDecode(jsonString);
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
不幸的是,jsonDecode()
返回一个 Map<String, dynamic>
,这意味着你在运行时以前都不知道值的类型。使用这个方法,你失去了大部分的静态类型语言特性:类型安全,自动补全以及最重要的编译时异常。你的代码会立即变得更加容易出错。
例如,当你获取 name
或者 email
字段,你可能很快引入一个笔误。然而编译器却无法知道 map 中有 JSON 笔误,编译器并不知道这个笔误。
在模型类中序列化 JSON 数据
通过引入简单的模型类来解决上面提到的问题,在这个例子中叫做 User
。在 User
类中,你会发现:
一个
User.fromJson()
构造函数,用于从 map 结构中构造一个新的User
实例一个
toJson()
方法,这个方法会将User
实例转换为一个 map
通过这种方法,调用代码 可以拥有类型安全,name
和 email
字段的自动完成以及编译时异常(检测)。如果你发生了笔误或者把 String
类型的字段看成了 int
类型,app 将不会编译,而不是在运行时崩溃。
user.dart
class User {
final String name;
final String email;
User(this.name, this.email);
User.fromJson(Map<String, dynamic> json)
: name = json['name'],
email = json['email'];
Map<String, dynamic> toJson() =>
{
'name': name,
'email': email,
};
}
解码逻辑的责任现在移动到了模型内部。通过这个新方法,你可以很容易地解码一个 user。
Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
要编码 user,将 User
对象传到 jsonEncode()
函数中。你不需要调用 toJson()
方法,因为 jsonEncode()
已经帮你做了这件事。
String json = jsonEncode(user);
通过这种方法,被调用的代码根本不需要担心序列化 JSON 数据的问题。然而,模型类仍然是必须的。在一个生产环境下的 App,你可能希望确保序列化数据能正确奏效。在实践中,User.fromJson()
和User.toJson()
方法都需要单元测试以便验证正确的行为。
这篇 cookbook 包含了更加全面的使用 JSON model 类的实用样例,它将使用 isolate 在后台线程解析 JSON 文件。若你在解析 JSON 文件的同时需要应用保持响应,这是理想的解决方案。
然而,现实场景通常不是那么简单,有时候响应的 JSON API 会更加复杂,例如它可能会包含一些相邻的 JSON 对象,而这些对象同样需要使用它的 model 类进行解析。
如果有一些东西可以帮你处理 JSON 编码和解码就好了。幸运的是,已经有了!
使用代码生成库序列化 JSON 数据
尽管有其它库可以使用,本指南使用了json_serializable package,一个自动化源代码生成器来为你生成 JSON 序列化数据模板。
由于序列化数据代码不再需要手动编写或者维护,你可以将序列化 JSON 数据在运行时的异常风险降到最低。
在项目中设置 json_serializable
要在你的项目中包含 json_serializable
,你需要一个常规依赖,以及两个 dev 依赖。简单来说,dev 依赖是不包括在我们的 App 源代码中的依赖 - 它们只会被用在开发环境中。
在序列化 JSON 数据的例子中,这些必须的依赖的最新版本可以在下面 [pubspec 文件][the pubspec file] 中查看。
pubspec.yaml
dependencies:
# Your other regular dependencies here
json_annotation: ^2.0.0
dev_dependencies:
# Your other dev_dependencies here
build_runner: ^1.0.0
json_serializable: ^2.0.0
在你的项目根文件夹下运行 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);
- }
通过这个设置,源代码生成器将生成用于 JSON 编码及解码 name
以及 email
字段的代码。
如果需要,你可以很容易自定义命名策略。例如,如果 API 返回带有蛇形命名方式的对象,并且你想要在你的模型里使用 小驼峰 的命名方式,你可以使用带有一个 name 参数的 @JsonKey
注解。
/// Tell json_serializable that "registration_date_millis" should be
/// mapped to this property.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
运行代码生成工具
当你首次创建 json_serializable
类时,你会得到类似下图的错误。
这些错误完全正常,很简单,因为这些模型类的生成代码并不存在。要解决这个问题,运行代码生成器来生成序列化数据模板。
有两种方式运行代码生成器。
一次性代码生成
通过在项目根目录运行 flutter pub run build_runner build
,你可以在任何需要的时候为你的模型生成 JSON 序列化数据代码。这会触发一次构建,遍历源文件,选择相关的文件,然后为它们生成必须的序列化数据代码。
虽然这样很方便,但是如果你不需要在每次修改了你的模型类后都要手动构建那将会很棒。
持续生成代码
监听器 让我们的源代码生成过程更加方便。它监听我们项目中的文件变化并且会在需要的时候自动构建必要的文件。通过在项目根目录运行 flutter pub run build_runner watch
启动监听。
一旦启动监听并让它留在后台运行是安全的。
使用 json_serializable 模型
为了以 json_serializable
的方式解码 JSON 字符串,事实上你不必对以前的代码做任何的改动。
Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);
编码也是如此。调用 API 和以前一样。
String json = jsonEncode(user);
使用 jsonserializable
,在 User
类中你可以忘记手动序列化任意的 JSON 数据。源代码生成器会创建一个名为 user.g.dart
的文件,它包含了所有必须的序列化数据逻辑。你不必再编写自动化测试来确保序列化数据奏效。现在由 库来负责_ 确保序列化数据能正确地奏效。
为嵌套类 (Nested Classes) 生成代码
你可能类在代码中用了嵌套类,在你把类作为参数传递给一些服务(比如 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 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
String firstName;
Address address;
User(this.firstName, this.address);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
在终端中运行 flutter pub run buildrunner build
创建 * .g.dart
文件,但私有函数如 $ UserToJson()
会看起来像下面这样:
(
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'firstName': instance.firstName,
'address': instance.address,
};
看起来没有什么问题,如果 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 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable(explicitToJson: true)
class User {
String firstName;
Address address;
User(this.firstName, this.address);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
了解更多信息,请查阅 json_annotation 这个 package 里的 JsonSerializable 类的 explicitToJson 参数等相关文档。
进一步参考
更多信息,请查看以下资源:
dart:convert 和 JsonCodec 文档
[Flutter 中有关 dart:mirrors 的讨论][discussion about dart:mirrors in Flutter]