持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第25天,点击查看活动详情
freezed | Dart Package (flutter-io.cn)
译时版本:2.2.0
Union 类型和封装类
这来自于其它语言,你可能习惯了如 "union 类型"/"封装类"/模式匹配 的特性。
这些是结合类型系统的强大工具,但是 Dart 现在还不支持这些。
但是,不用担心,Freezed 支持这些,会生成一些工具类帮助你。
长说短说,在任何一个 Freezed 类中,你可以编写多个构造方法:
@freezed
class Union with _$Union {
const factory Union.data(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = Error;
}
这样做,我们的 Model 现在可以处于不同的互斥状态。
特别是,该代码片段定义了一个 Model Union ,这个 Model 有三个可能的状态。
- data (数据状态)
- loading(加载中)
- error(错误)
注意我们在定义的工厂构造方法的右边定义了有实际意义的名字。 这会在后面方便使用。
另外一件也需要注意的是,在该示例中,我们不再需要如下写代码:
void main() {
Union union = Union.data(42);
print(union.value); // 编译错误:属性值不存在
}
让我们在下面的部分看一下为什么为这样。
当我们定义多个构造方法时,就不再能够读取不是所有构造方法共有的属性:
例如,如果编写如下代码:
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
const factory Example.city(String name, int population) = City;
}
这样你就不能再直接读取 age 和 population :
var example = Example.person('Remi', 24);
print(example.age); // 不能编译!
另一方面,你能够读取在所有的构造方法里都定义的属性。
例如, name 变量是 Example.person 和 Example.city 构造方法的共通属性。
因此,我们可以如下编写:
var example = Example.person('Remi', 24);
print(example.name); // Remi
example = Example.city('London', 8900000);
print(example.name); // London
同样的逻辑也适用于 copyWith 。
我们可以对所有的构造方法里都定义的属性使用 copyWith :
var example = Example.person('Remi', 24);
print(example.copyWith(name: 'Dash')); // Example.person(name: Dash, age: 24)
example = Example.city('London', 8900000);
print(example.copyWith(name: 'Paris')); // Example.city(name: Paris, population: 8900000)
另一方面,只在某个特定的构造方法里独有的属性是不可用的:
var example = Example.person('Remi', 24);
example.copyWith(age: 42); // 编译错误,参数 `age` 不存在
要解决这个问题,我们需要用叫做 “模式匹配” 的机制检查正在使用的对象的状态。
对于该部分,我们考虑下面的 Union:
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
const factory Example.city(String name, int population) = City;
}
现在看一下,我们如何使用模式匹配来读取一个 Example 实例的内容。
对于该问题,我们有几个方案:
When
when 方法等同于使用解构进行模式匹配。
方法的原型依赖于定义的构造方法。
例如,有如下代码:
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = ErrorDetails;
}
然后 when 会如下:
var union = Union(42);
print(
union.when(
(int value) => 'Data $value',
loading: () => 'loading',
error: (String? message) => 'Error: $message',
),
); // Data 42
但是如果我们定义:
@freezed
class Model with _$Model {
factory Model.first(String a) = First;
factory Model.second(int b, bool c) = Second;
}
when 会如下:
var model = Model.first('42');
print(
model.when(
first: (String a) => 'first $a',
second: (int b, bool c) => 'second $b $c'
),
); // first 42
注意,每个回调如何匹配构造方法的名称和原型。
注意:
所有的回调都是必需的并且不能为 null 。
如果不想这样,考虑使用 maybeWhen。
Map
考虑有下面这个类:
@freezed
class Model with _$Model {
factory Model.first(String a) = First;
factory Model.second(int b, bool c) = Second;
}
对于这个类,使用 when 会如下:
var model = Model.first('42');
print(
model.when(
first: (String a) => 'first $a',
second: (int b, bool c) => 'second $b $c'
),
); // first 42
使用map 会变成如下:
var model = Model.first('42');
print(
model.map(
first: (First value) => 'first ${value.a}',
second: (Second value) => 'second ${value.b} ${value.c}'
),
); // first 42
如果要进行复杂操作的话,这会很有用。就像 copyWith/toString 。例如:
var model = Model.second(42, false)
print(
model.map(
first: (value) => value,
second: (value) => value.copyWith(c: true),
)
); // Model.second(b: 42, c: true)
使用 is/as 读取 Freezed 类的内容
作为代替方案,一个(不可取的)方案是使用 is/as 关键字。
更明确的内容,可如下编写:
void main() {
Example value;
if (value is Person) {
// 使用 `is` ,这会让编译器知道 "value" 是一个 Person 实例。
// 因此我们可以读取它的所有属性。
print(value.age);
value = value.copyWith(age: 42);
}
// 作为代替方案, 如果我们能明确知道一个对象的类型,可以使用 `as` :
Person person = value as Person;
print(person.age);
}
注意:
使用 is 和 as ,虽然可行,但不推荐。
原因是它们并不 “彻底”。查看 www.fullstory.com/blog/discri…
用于 Union 类型的单独的类的混入和接口
如果同一个类有多个类型,你可能想要使其中一个类型实现接口或混入一个类。
可以使用 @Implements 修饰符或分别使用 @With 做到。
该例中,City 实现了 GeographicArea 。
abstract class GeographicArea {
int get population;
String get name;
}
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
@Implements<GeographicArea>()
const factory Example.city(String name, int population) = City;
}
这对于实现或混入泛型类都适用,例如 AdministrativeArea<House> 期望的是类有一个泛型类型的参数,例如 AdministrativeArea<T> 。这种情况下,Freezed 会生成正确的代码,但是 Dart 在编译时会抛出注解声明的加载错误。这避免这种问题,应该如下使用 @Implements.fromString 和 @With.fromString 修饰符:
abstract class GeographicArea {}
abstract class House {}
abstract class Shop {}
abstract class AdministrativeArea<T> {}
@freezed
class Example<T> with _$Example<T> {
const factory Example.person(String name, int age) = Person<T>;
@With.fromString('AdministrativeArea<T>')
const factory Example.street(String name) = Street<T>;
@With<House>()
@Implements<Shop>()
@Implements<GeographicArea>()
@Implements.fromString('AdministrativeArea<T>')
const factory Example.city(String name, int population) = City<T>;
}
注意: 要确保需要实现接口的所有抽象成员。 如果接口没有成员或字段,可以通过添加 Unoin 类型的构造方法满足接口的约定。 要记住如果接口定义了一个需要在类中实现的方法或 getter ,需要参考向 Model 添加 getter 和方法 的使用说明.
注意 2:
不能对 Freezed 类使用 @With/@Implements 。
Freezed 类不能被继承或实现。