[译]Flutter Favorite 之超给力的辅助代码生成器 freezed - 使用 Freezed 创建 Model

4,331 阅读3分钟

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