[译]Flutter Favorite 之超给力的辅助代码生成器 freezed - Union 类型 和 封装类

538 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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;
}

这样你就不能再直接读取 agepopulation :

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 实例的内容。

对于该问题,我们有几个方案:

  • (推荐) 使用 Freezed 生成的方法 (when/map)查阅对象的内容。
  • (不推荐) 使用 is/as 把一个 Example 变量转换为 Person 实例或 City 实例。

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

map 方法和 when 是等同的,但是 没有 解构。

考虑有下面这个类:

@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 类不能被继承或实现。