持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情
freezed | Dart Package (flutter-io.cn)
译时版本:2.2.0
欢迎使用 Freezed ,另外一个用于 数据类/Union/模式匹配/克隆 的代码生成器。
创建使用 Freezed 的 Model
示例胜于冗长抽象的解释,所以这里有一个典型的 Freezed 类:
// 该文件是 "main.dart"
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
// 必需:将 Freezed 生成的代码和 `main.dart` 绑定。
part 'main.freezed.dart';
// 可选:因为这里的 Person 类是可序列化的,必须添加该行。
// 如果 Person 不是可序列化的,可以跳过该行。
part 'main.g.dart';
@freezed
class Person with _$Person {
const factory Person({
required String firstName,
required String lastName,
required int age,
}) = _Person;
factory Person.fromJson(Map<String, Object?> json)
=> _$PersonFromJson(json);
}
以下的代码片段定义了名为 Person
的 Model :
-
Person
有3个属性:firstName
、lastName
和age
-
因为我们在使用
@freezed
,该类的所有属性都是不可更改的。 -
因为我们定义了
fromJson
,所以该类是可序列化/可反序列化的。 Freezed 会为我们添加toJson
方法。 -
Freezed 会自动生成:
copyWith
方法,用于克隆有不同属性的对象toString
的覆写,列出对象的所有属性。- 操作符
==
和hashCode
的覆写 (因为Person
是不可更改的)
从该示例中,我们注意到一些事情:
- 必须使用
@freezed
(或@Freezed
/@unfreezed
,后面有更多内容)注解Model。
该注解会告诉 Freezed 要为该类生成代码。 - 我们必须要使用带我们类名的混入,使用
_$
作为前缀。该混入定义了对象的各种属性/方法。 - 在 Freezed 类中定义构造方法时,我们就该使用所示代码中的
factory
关键字 (const
是可选的)。
该构造方法的参数会是该类包含的所有属性的列表。
参数 不必是 被命名和必须的。如果需要可自由使用占位的可选参数。
定义可更改的类代替不可更改的类
到现在为止,我们已经看到了如何定义一个所有属性都是 final
的 Model;但是你也可能想要在 Model 中定义可更改的属性。
Freezed 支持这种定义,把 @freezed
注解换成 @unfreezed
即可:
@unfreezed
class Person with _$Person {
factory Person({
required String firstName,
required String lastName,
required final int age,
}) = _Person;
factory Person.fromJson(Map<String, Object?> json)
=> _$PersonFromJson(json);
}
这几乎定义了一个和前面的代码片段一样的 Model,但是有下面的区别:
-
firstName
和lastName
现在是可更改的。因此,我们可以写:void main() { var person = Person(firstName: 'John', lastName: 'Smith', age: 42); person.firstName = 'Mona'; person.lastName = 'Lisa'; }
-
age
仍然是不可更改的,因为我们明确地将该属性设为了final
。 -
Person
不再有自定义的 ==/hashCode 的实现。void main() { var john = Person(firstName: 'John', lastName: 'Smith', age: 42); var john2 = Person(firstName: 'John', lastName: 'Smith', age: 42); print(john == john2); // false }
-
当然,因为
Person
类是可更改的,它也不能使用const
初始化。
允许 List/Map/Set 的更改
默认当使用 @freezed
(但不是 @unfreezed
)时, List
/Map
/Set
类型的属性会转换为不可更改的。
这意味着下面的代码会导致运行时异常:
@freezed
class Example with _$Example {
factory Example(List<int> list) = _Example;
}
void main() {
var example = Example([]);
example.list.add(42); // 抛出异常因为我们在更改一个集合。
}
该行为可通过以下改写禁止:
@Freezed(makeCollectionsUnmodifiable: false)
class Example with _$Example {
factory Example(List<int> list) = _Example;
}
void main() {
var example = Example([]);
example.list.add(42); // OK
}
copyWith 的工作机制
正如前面说明的,使用 Freezed 定义一个 Model 时,代码生成器会自动为我们生成 copyWith
方法。
该方法用于克隆带有不同值的对象。
例如如果我们定义了:
@freezed
class Person with _$Person {
factory Person(String name, int? age) = _Person;
}
然后我们可以如下编写:
void main() {
var person = Person('Remi', 24);
// `age` 没有传递,它的值会被保持
print(person.copyWith(name: 'Dash')); // Person(name: Dash, age: 24)
// `age` 改为 `null`
print(person.copyWith(age: null)); // Person(name: Remi, age: null)
}
注意,Freezed 支持 person.copyWith(age: null)
。
进阶:深拷贝
虽然 copyWith
本身非常强大,但对于复杂些的对象很不方便。
考虑下面的类:
@freezed
class Company with _$Company {
factory Company({String? name, required Director director}) = _Company;
}
@freezed
class Director with _$Director {
factory Director({String? name, Assistant? assistant}) = _Director;
}
@freezed
class Assistant with _$Assistant {
factory Assistant({String? name, int? age}) = _Assistant;
}
然后,从 Company
的引用,我们想要反映改动到 Assistant
。
例如,要改变 assistant 的 name
,使用 copyWith
,我们得如下编写:
Company company;
Company newCompany = company.copyWith(
director: company.director.copyWith(
assistant: company.director.assistant.copyWith(
name: 'John Smith',
),
),
);
这是 可行的 ,但是带有大量复制导致相对冗长。
这也是我们需要使用 Freezed 的 “深拷贝” 的地方。
如果一个 Freezed Model 包含的属性也是 Freezed 的 Model ,然后代码生成器会为前面的示例提供可替换的语法:
Company company;
Company newCompany = company.copyWith.director.assistant(name: 'John Smith');
该代码片段会实现和前面的代码完全一样的结果(创建新的 company 使用更新后的 assistant 的 name),但是没有这么多复制。
深入该方法,如果是要改变 director 的 name ,我们可以写成:
Company company;
Company newCompany = company.copyWith.director(name: 'John Doe');
总的来说,基于上面提到的 Company
/Director
/Assistant
的定义,所有以下的 “copy” 语法都可行:
Company company;
company = company.copyWith(name: 'Google', director: Director(...));
company = company.copyWith.director(name: 'Larry', assistant: Assistant(...));
有关 Null 的考虑
一些对象也可能是 null
。例如,使用我们的 Company
类,
Director
的 assistant
可能是 null
。
因此,编写如下代码:
Company company = Company(name: 'Google', director: Director(assistant: null));
Company newCompany = company.copyWith.director.assistant(name: 'John');
会没有意义。
如果还没有 assistant 我们不能改变 assistant 的 name。
这种情况下, company.copyWith.director.assistant
会返回 null
,导致代码编译失败。
要修复该问题,可以使用 ?.call
操作符,编写代码如下:
Company? newCompany = company.copyWith.director.assistant?.call(name: 'John');
为 Model 添加 Getter 和 方法
有时,可能想要在类中手动定义方法或属性。
但是很快你会发现如果你试着如下去做:
@freezed
class Person with _$Person {
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
它并不起作用。
要使其起作用,我们需要额外的步骤:我们需要定义一个私有的空的构造方法:
@freezed
class Person with _$Person {
// 添加构造方法。必须不带任何参数。
const Person._();
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
断言
Dart 不允许向 factory
(工厂)的构造方法中添加 assert(...)
语句。
因此, 要向 Freezed 类中添加断言,需要 @Assert
修饰符:
class Person with _$Person {
@Assert('name.isNotEmpty', 'name cannot be empty')
@Assert('age >= 0')
factory Person({
String? name,
int? age,
}) = _Person;
}
默认值
和断言类似,Dart 不允许 “重定向工厂构造方法” 来指定默认值。
因此,如果想要指定属性的默认值,需要 @Default
注解:
class Example with _$Example {
const factory Example([@Default(42) int value]) = _Example;
}
注意:
如果正在使用序列化/反序列化,这会为你自动添加上 @JsonKey(defaultValue: <something>)
。
修饰符和注释
Freezed 通过为各个参数和构造方法的定义添加 修饰/文档 来支持属性和类级别的修饰符/文档。
考虑下面的代码:
@freezed
class Person with _$Person {
const factory Person({
String? name,
int? age,
Gender? gender,
}) = _Person;
}
如果想要对 name
添加文档,可以这样做:
@freezed
class Person with _$Person {
const factory Person({
/// 用户名
///
/// 不能为空
String? name,
int? age,
Gender? gender,
}) = _Person;
}
如果想要把属性 gender
标记为 @deprecated
,可以这样做:
@freezed
class Person with _$Person {
const factory Person({
String? name,
int? age,
@deprecated Gender? gender,
}) = _Person;
}
同时以下内容也会不再推荐使用:
-
构造方法
Person(gender: Gender.something); // gender 是不再推荐使用的
-
生成类的构造方法:
_Person(gender: Gender.something); // gender 是不再推荐使用的
-
属性:
Person person; print(person.gender); // gender is deprecated
-
copyWith
参数:Person person; person.copyWith(gender: Gender.something); // gender 是不再推荐使用的
类似的,如果想要修饰生成类,可以修饰定义的工厂构造方法。
因此,要废止 _Person
,可以如下:
@freezed
class Person with _$Person {
@deprecated
const factory Person({
String? name,
int? age,
Gender? gender,
}) = _Person;
}