在这篇文章中,我们将学习如何使用Freezed包用代码生成来解析JSON数据。而且我还会分享一个VS Code扩展,让这个过程更加简单。 🙂
准备好了吗?开始吧!
安装 codegen 依赖项
为了让事情顺利进行,我们需要在我们的pubspec.yaml 文件中添加一些依赖项。
dependencies:
flutter:
sdk: flutter
freezed_annotation: ^0.14.2
json_annotation: ^4.0.1
dev_dependencies:
build_runner: ^2.0.6
freezed: ^0.14.2
json_serializable: ^4.1.4
5个新的依赖项?真的吗?
是的,它们的作用是这样的。
json_serializable:为Dart构建系统提供了一些处理JSON的构建器json_annotation: 定义了Dart构建系统所使用的注解json_serializablefreezed:一个强大的代码生成器,可以用简单的API处理复杂的用例。freezed_annotation定义了Dart构建系统所使用的注解。freezedbuild_runner:这是一个独立的构建包,可以为我们生成Dart文件。
听起来很复杂?别担心:只要你导入所有需要的包,就可以了。👍
你可以单独用
json_serializable来生成JSON解析代码(不用freezed)。然而,freezed更加强大,可以用一个简单的API处理复杂的用例。
一个JSON文档的例子
为了与之前的文章保持一致,我们将重复使用相同的JSON样本。
{
"name": "Pizza da Mario",
"cuisine": "Italian",
"year_opened": 1990,
"reviews": [
{
"score": 4.5,
"review": "The pizza was amazing!"
},
{
"score": 5.0,
"review": "Very friendly staff, excellent service!"
}
]
}
作为参考,这里是我们之前写的Restaurant 和Review 模型类。
class Restaurant {
Restaurant({
required this.name,
required this.cuisine,
this.yearOpened,
required this.reviews,
});
final String name;
final String cuisine;
final int? yearOpened;
final List<Review> reviews;
factory Restaurant.fromMap(Map<String, dynamic> data) {
final name = data['name'] as String;
final cuisine = data['cuisine'] as String;
final yearOpened = data['year_opened'] as int?;
final reviewsData = data['reviews'] as List<dynamic>?;
final reviews = reviewsData != null
? reviewsData.map((reviewData) => Review.fromMap(reviewData)).toList()
: <Review>[];
return Restaurant(
name: name,
cuisine: cuisine,
yearOpened: yearOpened,
reviews: reviews,
);
}
Map<String, dynamic> toMap() {
return {
'name': name,
'cuisine': cuisine,
if (yearOpened != null) 'year_opened': yearOpened,
'reviews': reviews.map((review) => review.toMap()).toList(),
};
}
}
class Review {
Review({required this.score, this.review});
final double score;
// nullable - assuming the review may be missing
final String? review;
factory Review.fromMap(Map<String, dynamic> data) {
final score = data['score'] as double;
final review = data['review'] as String?;
return Review(score: score, review: review);
}
Map<String, dynamic> toMap() {
return {
'score': score,
if (review != null) 'review': review,
};
}
}
我们可以看到,这是一个很大的代码,如果我们有很多不同的模型,这种方法就无法扩展。
带有Freezed的模型类
为了使我们的生活更轻松,让我们使用Freezed来定义我们的Restaurant 和Review 模型类。
由于Restaurant 依赖于 Review ,让我们从Review 类开始。
// review.dart
// 1. import freezed_annotation
import 'package:freezed_annotation/freezed_annotation.dart';
// 2. add 'part' files
part 'review.freezed.dart';
part 'review.g.dart';
// 3. add @freezed annotation
@freezed
// 4. define a class with a mixin
class Review with _$Review {
// 5. define a factory constructor
factory Review({
// 6. list all the arguments/properties
required double score,
String? review,
// 7. assign it with the `_Review` class constructor
}) = _Review;
// 8. define another factory constructor to parse from json
factory Review.fromJson(Map<String, dynamic> json) => _$ReviewFromJson(json);
}
在这里使用正确的语法是非常重要的。如果我们遗漏了什么或添加了一个错别字,我们的代码生成器将产生一些错误。
让我们对Restaurant 类进行同样的处理。
// restaurant.dart
import 'package:freezed_annotation/freezed_annotation.dart';
// import any other models we depend on
import 'review.dart';
part 'restaurant.freezed.dart';
part 'restaurant.g.dart';
@freezed
class Restaurant with _$Restaurant {
factory Restaurant({
required String name,
required String cuisine,
// note: using a JsonKey to map our JSON key that uses
// *snake_case* to our Dart variable that uses *camelCase*
@JsonKey(name: 'year_opened') int? yearOpened,
// note: using an empty list as a default value
@Default([]) List<Review> reviews,
}) = _Restaurant;
factory Restaurant.fromJson(Map<String, dynamic> json) =>
_$RestaurantFromJson(json);
}
注意Restaurant 和Review 类都有一个工厂构造函数,列出了我们需要的所有参数,但我们还没有声明相应的属性。
事实上,我们的代码是不完整的,会产生像这样的错误。
Target of URI doesn't exist: 'restaurant.freezed.dart'.
Try creating the file referenced by the URI, or Try using a URI for a file that does exist.
The name '_Restaurant' isn't a type and can't be used in a redirected constructor.
Try redirecting to a different constructor.
The method '_$RestaurantFromJson' isn't defined for the type 'Restaurant'.
Try correcting the name to the name of an existing method, or defining a method named '_$RestaurantFromJson'.
让我们来处理一下这个问题。
运行代码生成器
为了生成缺失的代码,我们可以在控制台运行这个。
flutter pub run build_runner build --delete-conflicting-outputs
这将产生以下输出。
[INFO] Generating build script...
[INFO] Generating build script completed, took 419ms
[INFO] Initializing inputs
[INFO] Reading cached asset graph...
[INFO] Reading cached asset graph completed, took 55ms
[INFO] Checking for updates since last build...
[INFO] Checking for updates since last build completed, took 428ms
[INFO] Running build...
[INFO] 1.3s elapsed, 0/2 actions completed.
[INFO] Running build completed, took 2.1s
[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 27ms
[INFO] Succeeded after 2.1s with 5 outputs (5 actions)
如果我们在项目资源管理器中查看,我们可以发现一些新文件。
restaurant.dart
restaurant.freezed.dart
restaurant.g.dart
review.dart
review.freezed.dart
review.g.dart
.freezed.dart 文件包含大量的代码。如果你想看所有生成的代码,你可以查看这个gist。
重要的是,对于每个模型类,代码生成器都添加了。
- 所有我们需要的存储属性(并使它们成为
final)。 toString()方法==操作符hashCodegetter变量copyWith()方法toJson()方法
相当方便!
如果我们需要修改我们模型类中的任何属性,我们只需要更新它们的工厂构造函数。
@freezed
class Review with _$Review {
factory Review({
// update any properties as needed
required double score,
String? review,
}) = _Review;
factory Review.fromJson(Map<String, dynamic> json) => _$ReviewFromJson(json);
}
@freezed
class Restaurant with _$Restaurant {
factory Restaurant({
// update any properties as needed
required String name,
required String cuisine,
@JsonKey(name: 'year_opened') int? yearOpened,
@Default([]) List<Review> reviews,
}) = _Restaurant;
factory Restaurant.fromJson(Map<String, dynamic> json) =>
_$RestaurantFromJson(json);
}
然后我们可以再次运行代码生成器,Freezed会处理剩下的事情。
flutter pub run build_runner build --delete-conflicting-outputs
Viola!我们现在可以在几行代码中定义类型安全、不可变的模型类,并通过运行一个命令生成所有的JSON序列化代码。
基本的JSON注释
Freezed支持许多注释,让我们自定义代码生成器如何处理我们的模型。
最有用的是@JsonKey 和@Default 。
下面是我在GitHub上的电影应用中使用它们的例子。
@freezed
class TMDBMovieBasic with _$TMDBMovieBasic {
factory TMDBMovieBasic({
@JsonKey(name: 'vote_count') int? voteCount,
required int id,
@Default(false) bool video,
@JsonKey(name: 'vote_average') double? voteAverage,
required String title,
double? popularity,
@JsonKey(name: 'poster_path') required String posterPath,
@JsonKey(name: 'original_language') String? originalLanguage,
@JsonKey(name: 'original_title') String? originalTitle,
@JsonKey(name: 'genre_ids') List<int>? genreIds,
@JsonKey(name: 'backdrop_path') String? backdropPath,
bool? adult,
String? overview,
@JsonKey(name: 'release_date') String? releaseDate,
}) = _TMDBMovieBasic;
factory TMDBMovieBasic.fromJson(Map<String, dynamic> json) =>
_$TMDBMovieBasicFromJson(json);
}
在这种情况下,JSON响应中的键使用了snake_case 命名惯例,我们可以使用@JsonKey 注解来告诉Freezed哪些键被映射到哪些属性。
如果我们想为一个给定的非空值属性指定一个默认值,我们可以使用@Default 注释。
非空值参数需要一个
required关键字或者一个默认值。如果指定了@Default注释,如果缺少相应的JSON键值对,将使用其值。
使用Freezed的高级JSON序列化功能
到目前为止,我们所涉及的已经足够在大多数情况下处理JSON序列化。
但是Freezed是一个强大的包,我们可以做一些很酷的事情,比如。
- 通过指定多个构造函数来生成联合类型
- 指定自定义的JSON转换器
要了解最先进的功能,请阅读文档中的这一部分。
代码生成的弊端
代码生成有一些明显的好处,如果你有很多模型类,这是一个好办法。
但也有一些弊端。
大量的额外代码
我们的Restaurant 和Review 模型类非常简单,但是生成的代码已经占用了450行的代码。如果你有很多模型类,这很快就会增加。
代码生成很慢
Dart的代码生成是相当慢的。
尽管有一些方法可以缓解这个问题,但在大项目中,codegen会大大减慢你的开发工作流程。
生成的文件应该被添加到git中吗?
如果你在一个团队中工作,并将生成的文件提交到git,那么拉动请求就会变得更难审查。
但如果你不这样做,项目默认就不处于可运行状态,而且。
- 每个团队成员都需要记住运行codegen步骤(有可能导致不一致)。
- 需要一个自定义的CI构建步骤来构建该应用
而根据这个投票,对于是否应该将生成的文件添加到git中,甚至没有达成共识。
Dart语言的局限性
代码生成可以提供帮助,但它不是银弹。
根本的问题是Dart(还)没有任何语言特性可以使JSON序列化更容易。
数据类的引入--以及Dart中更广泛的静态元编程--有可能解决这些问题。所以我们可以希望在未来,JSON序列化在Dart中会变得更加容易。
但鉴于目前的语言限制,用Freezed生成代码是我们最好的选择。
VSCode扩展。Json到Dart模型
正如我们所了解的,我们可以用工厂构造函数声明一些模型类,并让代码生成器来处理其他的事情。
但是如果我们可以直接从JSON样本文件中生成一切,那不是很酷吗?
那么,Json to Dart ModelVSCode扩展正是这样做的。
试过之后,我可以说,它的工作做得很好,而且它支持Freezed。因此,如果你想节省一些额外的时间,可以考虑使用它。
还有一个叫QuickType的在线工具,可以在任何语言中生成类型安全的模型类。但它现在还不支持Dart Null Safety。
总结
我们现在已经探索了JSON序列化的各种选项。
这里有一些指导原则来帮助你选择最好的方法。
- 如果你有几个小的模型类,你可以手工编写JSON解析代码。
- 如果你有很多模型类,Freezed可以为你完成繁重的工作。
- 为了进一步加快速度,可以考虑使用VS Code的Json to Dart Model扩展。
编码愉快!