不管在什么样的应用中,如果要处理大量的数据,不可避免的就是要定义大量的数据类用来装载和解析数据,在 Flutter 中也不例外,今天要介绍的这个 Freezed 库就是 Flutter 中用来作为数据类(data classes)代码生成的这样一款工具。

freezed 是什么

freezed 是一个 Flutter/Dart 生态系统中一个非常强大的代码生成工具,用于创建数据类,基于 Dart 的代码生成功能,通过自动生成 data classes, tagged unions, nested classes 和 clone 代码模板,大大减少了手动编写重复性代码的工作量。freezed 的设计非常类似 Java 生态中的 [[Lombok]],通过注解和代码生成来减少样板代码量。

尽管 Dart 很棒,但是在定义 Model 的时候还是非常乏味,编程者需要

  • 定义构造函数,属性
  • 重载 toString, == hashCode 等
  • 实现 copyWith 方法来 clone 对象
  • 处理序列化以及反序列化等

如果要实现这一些,一个 Model 可能就需要上百行代码,这不仅容易出错,而且还影响了代码易读性。freezed 就是设计用来来帮助开发者只需要关注定义 Model,而无需考虑其他。

比如一个简单的用户定义

class Person with _$Person {
  Person({
    required this.firstName;
    required this.lastName;
    required this.age;
  })
  final String firstName;
  final String lastName;
  final String age;
}

freezed 安装

为了使用 Freezed,需要依赖 build_runner 代码生成,首先安装 build_runner 和 Freezed,添加依赖到 pubspec.yaml 中。

通过命令行添加

flutter pub add freezed_annotation
flutter pub add dev:build_runner
flutter pub add dev:freezed
# if using freezed to generate fromJson/toJson, also add:
flutter pub add json_annotation
flutter pub add dev:json_serializable

直接修改 pubspec.yaml 文件

dependencies:
  flutter:
    sdk: flutter
  freezed_annotation: ^3.0.4
  json_annotation: ^4.0.6  # 如需JSON序列化支持

dev_dependencies:
  build_runner: ^2.3.2
  freezed: ^3.0.4
  json_serializable: ^6.5.4  # 如需JSON序列化支持

然后运行 flutter pub get

说明

  • build_runner 用来运行代码生成
  • freezed 是代码生成器
  • freezed_annotation,包含了 freezed 注解

运行代码生成

dart run build_runner watch -d
# or
flutter pub run build_runner build --delete-conflicting-outputs

和其他代码生成器需要的一样,freezed 需要导入 freezed 注解,并且需要使用 part 关键字

import 'package:freezed_annotation/freezed_annotation.dart';

part 'my_file.freezed.dart';

freezed 使用

为了避免与 JSON 序列化相关的警告,建议在项目根目录的analysis_options.yaml文件中添加以下配置

analyzer:
  errors:
    invalid_annotation_target: ignore

创建 Model

Freezed 提供两种方式来创建 data-classes

  • Primary constructors,定义构造函数,Freezed 生成关联字段
  • Classic classes,编写正常的 Dart 类,Freezed 只生成 toString/ == / copyWith

Primary constructors

Freezed 实现 Primary Constructors 通过 factory 关键字。

import 'package:freezed_annotation/freezed_annotation.dart';

// required: associates our `main.dart` with the code generated by Freezed
part 'main.freezed.dart';
// optional: Since our Person class is serializable, we must add this line.
// But if Person was not serializable, we could skip it.
part 'main.g.dart';

@freezed
abstract 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

  • 属性
  • 使用了 @freezed 注解,类属性是不可变的
  • 定义了 fromJson,序列化和反序列化,Freezed 会添加 toJson 方法
  • Freezed 会自动生成 toString / == / hashCode / copyWith

执行命令

flutter pub run build_runner build --delete-conflicting-outputs

命令将生成 xxx.freezed.dart 文件,以及 xxx.g.dart 包含序列化相关的代码

如果有一些情况需要定义 getters 或者自定义方法,这个时候需要定义一个空的构造函数

@freezed
abstract class Person with _$Person {
  // Added constructor. Must not have any parameter
  const Person._();

  const factory Person(String name, {int? age}) = _Person;

  void method() {
    print('hello world');
  }
}

定义可变类

需要使用 @unfreezed

@unfreezed
abstract 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);
}

姓名是可变的

void main() {
  var person = Person(firstName: 'John', lastName: 'Smith', age: 42);

  person.firstName = 'Mona';
  person.lastName = 'Lisa';
}

并且不会实现 == / hashCode 方法,所以下面的比较等于 false

void main() {
  var john = Person(firstName: 'John', lastName: 'Smith', age: 42);
  var john2 = Person(firstName: 'John', lastName: 'Smith', age: 42);

  print(john == john2); // false
}

让 Lists/Maps/Sets 可变

通常情况下,如果使用 @freezed ,那么内部属性如果使用 List/Map/Set 会自动变成不可变

@freezed
abstract class Example with _$Example {
  factory Example(List<int> list) = _Example;
}

void main() {
  var example = Example([]);
  example.list.add(42); // throws because we are mutating a collection
}

需要调整为

@Freezed(makeCollectionsUnmodifiable: false)
abstract class Example with _$Example {
  factory Example(List<int> list) = _Example;
}

void main() {
  var example = Example([]);
  example.list.add(42); // OK
}

Classic classes

和之前提到的 Primary constructors 相比,这个模式下,我们只需要定义普通的 Dart classes。

通常编写 constructor 和 fields 定义。

import 'package:freezed_annotation/freezed_annotation.dart';

// required: associates our `main.dart` with the code generated by Freezed
part 'main.freezed.dart';
// optional: Since our Person class is serializable, we must add this line.
// But if Person was not serializable, we could skip it.
part 'main.g.dart';

@freezed
@JsonSerializable()
class Person with _$Person {
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  final String firstName;
  final String lastName;
  final int age;

  factory Person.fromJson(Map<String, Object?> json)
      => _$PersonFromJson(json);

  Map<String, Object?> toJson() => _$PersonToJson(this);
}

Freezed 会自动处理 copyWith / toString / == / hashCode 。

默认值 @Default

freezed 提供 @Default 注解来为属性设置默认值。使用 @Default 后,该属性在构造时可以不传入。

@freezed
abstract class Settings with _$Settings {
  const factory Settings({
    @Default('en') String language,
    @Default(true) bool darkMode,
    @Default(14) int fontSize,
  }) = _Settings;

  factory Settings.fromJson(Map<String, Object?> json) => _$SettingsFromJson(json);
}

使用时可以只传入需要修改的字段

void main() {
  // 使用全部默认值
  final defaultSettings = Settings();
  print(defaultSettings.language); // 'en'

  // 覆盖部分默认值
  final customSettings = Settings(language: 'zh', fontSize: 16);
}

在 freezed 3.0 中,还支持非常量默认值。通过使用 Mixed Mode(不使用 factory 构造函数),可以在构造函数中直接使用非常量的默认值

@freezed
abstract class Event with _$Event {
  Event({
    required this.name,
    DateTime? time,
  }) : time = time ?? DateTime.now();

  final String name;
  final DateTime time;
}

自定义 JSON 序列化 @JsonKey

freezed 配合 json_serializable 使用时,可以通过 @JsonKey 注解来自定义 JSON 字段的序列化和反序列化行为。

@freezed
abstract class User with _$User {
  const factory User({
    @JsonKey(name: 'user_name') required String userName,
    @JsonKey(name: 'created_at', fromJson: _dateFromJson, toJson: _dateToJson)
    required DateTime createdAt,
    @JsonKey(includeIfNull: false) String? nickname,
    @JsonKey(defaultValue: 0) int score,
  }) = _User;

  factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}

DateTime _dateFromJson(String date) => DateTime.parse(date);
String _dateToJson(DateTime date) => date.toIso8601String();

常用的 @JsonKey 参数

  • name:指定 JSON 中的字段名,用于处理命名风格不一致的情况(如 snake_case 和 camelCase)
  • fromJson / toJson:自定义序列化和反序列化函数
  • defaultValue:反序列化时如果字段缺失则使用的默认值
  • includeIfNull:为 false 时,序列化时忽略 null 值的字段
  • ignore:设为 true 时,该字段不参与序列化

如果需要处理自定义类型的转换,可以使用 JsonConverter

class DateTimeConverter implements JsonConverter<DateTime, String> {
  const DateTimeConverter();

  @override
  DateTime fromJson(String json) => DateTime.parse(json);

  @override
  String toJson(DateTime object) => object.toIso8601String();
}

@freezed
abstract class Post with _$Post {
  const factory Post({
    required String title,
    @DateTimeConverter() required DateTime publishedAt,
  }) = _Post;

  factory Post.fromJson(Map<String, Object?> json) => _$PostFromJson(json);
}

Union Types 联合类型

Union Types(联合类型)是 freezed 最核心的功能之一,也是它与其他代码生成工具(如 equatable)最大的区别。联合类型允许在一个类中定义多个不同形态的构造函数,每个构造函数可以携带不同的数据,非常适合用于状态管理。

定义联合类型时,需要使用 sealed 关键字(freezed 3.0 要求),并为每个构造函数指定一个不同的重定向类

@freezed
sealed class AuthState with _$AuthState {
  const factory AuthState.initial() = AuthInitial;
  const factory AuthState.loading() = AuthLoading;
  const factory AuthState.authenticated(User user) = Authenticated;
  const factory AuthState.error(String message) = AuthError;
}

联合类型最典型的使用场景是在 [[Flutter]] 的状态管理中,特别是配合 BLoC 使用

@freezed
sealed class ApiResult<T> with _$ApiResult<T> {
  const factory ApiResult.success(T data) = ApiSuccess<T>;
  const factory ApiResult.failure(String message, {int? statusCode}) = ApiFailure<T>;
  const factory ApiResult.loading() = ApiLoading<T>;
}

联合类型在 JSON 序列化时,默认使用 runtimeType 字段来区分不同的类型。可以通过 @Freezed 注解来自定义

@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.pascal)
sealed class MyResponse with _$MyResponse {
  const factory MyResponse.success(String data) = MyResponseSuccess;

  @FreezedUnionValue('SpecialCase')
  const factory MyResponse.special(String a, int b) = MyResponseSpecial;
}

上面的代码在序列化时会使用 type 作为区分字段,而非默认的 runtimeType

freezed 3.0 还引入了两个新特性

Eject Union Cases:可以为联合类型的某个分支使用自定义类,而不是让 freezed 自动生成

@freezed
sealed class Result<T> with _$Result<T> {
  Result._();

  // freezed 自动生成 ResultData
  factory Result.data(T data) = ResultData;

  // 使用已有的自定义类 ResultError
  factory Result.error(Object error) = ResultError;
}

// 自定义实现
class ResultError<T> extends Result<T> {
  ResultError(this.error) : super._();
  final Object error;

  @override
  String toString() => 'Custom error: $error';
}

私有构造函数:可以将某些联合分支设为私有

@freezed
sealed class Result<T> with _$Result<T> {
  factory Result._data(T data) = ResultData;
  factory Result.error(Object error) = ResultError;
}

模式匹配

在 freezed 2.x 中,联合类型提供了 whenmapmaybeWhenmaybeMap 等方法来进行模式匹配。freezed 3.0 移除了这些方法(后来在 3.2.0 中重新添加),推荐使用 Dart 原生的 pattern matching 语法。

使用 Dart 原生的 switch 表达式(推荐方式)

@freezed
sealed class AuthState with _$AuthState {
  const factory AuthState.initial() = AuthInitial;
  const factory AuthState.loading() = AuthLoading;
  const factory AuthState.authenticated(User user) = Authenticated;
  const factory AuthState.error(String message) = AuthError;
}

// 使用 switch 表达式
Widget buildWidget(AuthState state) {
  return switch (state) {
    AuthInitial() => const Text('Welcome'),
    AuthLoading() => const CircularProgressIndicator(),
    Authenticated(:final user) => Text('Hello, ${user.name}'),
    AuthError(:final message) => Text('Error: $message'),
  };
}

使用 switch 语句

void handleState(AuthState state) {
  switch (state) {
    case AuthInitial():
      print('initial');
    case AuthLoading():
      print('loading');
    case Authenticated(:final user):
      print('authenticated: ${user.name}');
    case AuthError(:final message):
      print('error: $message');
  }
}

Dart 原生的 pattern matching 相比 when/map 的优势在于编译器会进行穷举检查。如果你遗漏了某个分支,编译器会直接报错,而不是在运行时才发现问题。

如果仍然需要 when/map 方法(例如从 freezed 2.x 迁移的项目),在 freezed 3.2.0 及以上版本中这些方法已经作为扩展方法被重新添加。

深拷贝 Deep Copy

freezed 自动生成的 copyWith 方法支持深拷贝嵌套的 freezed 对象。当一个 freezed 类包含另一个 freezed 类作为属性时,copyWith 可以直接修改嵌套对象的内部字段,而无需手动展开。

@freezed
abstract class Address with _$Address {
  const factory Address({
    required String city,
    required String street,
  }) = _Address;
}

@freezed
abstract class Company with _$Company {
  const factory Company({
    required String name,
    required Address address,
  }) = _Company;
}

@freezed
abstract class Employee with _$Employee {
  const factory Employee({
    required String name,
    required Company company,
  }) = _Employee;
}

使用深拷贝

void main() {
  final employee = Employee(
    name: 'John',
    company: Company(
      name: 'Google',
      address: Address(city: 'Tokyo', street: 'Shibuya'),
    ),
  );

  // 深拷贝:直接修改嵌套对象的字段
  final movedEmployee = employee.copyWith.company.address(city: 'Osaka');
  print(movedEmployee.company.address.city); // 'Osaka'
  print(movedEmployee.company.name); // 'Google' (未修改的字段保持不变)
  print(movedEmployee.name); // 'John'

  // 如果没有深拷贝,需要手动展开
  // final movedEmployee = employee.copyWith(
  //   company: employee.company.copyWith(
  //     address: employee.company.address.copyWith(city: 'Osaka'),
  //   ),
  // );
}

深拷贝只对属性类型也是 freezed 类的情况有效。如果属性是 List<FreezedClass> 类型,则不支持直接深拷贝列表内的元素。

freezed 2.x 到 3.0 的迁移

freezed 3.0 于 2025 年 2 月发布,带来了一些重要的变化。

类声明方式变更:使用 factory 构造函数的类现在必须声明为 abstract(单一类)或 sealed(联合类型)

// freezed 2.x
@freezed
class Person with _$Person {
  const factory Person({required String name}) = _Person;
}

// freezed 3.0 - 单一类使用 abstract
@freezed
abstract class Person with _$Person {
  const factory Person({required String name}) = _Person;
}

// freezed 3.0 - 联合类型使用 sealed
@freezed
sealed class Result with _$Result {
  factory Result.success(String data) = Success;
  factory Result.error(String message) = Error;
}

Mixed Mode:3.0 引入了混合模式,允许不使用 factory 构造函数来定义类

@freezed
class Person with _$Person {
  Person({this.name, this.age});

  final String? name;
  final int? age;
}

map/when 方法:3.0 最初移除了这些方法,推荐使用 Dart 原生 pattern matching。在 3.2.0 版本中这些方法被重新添加为扩展方法,以帮助项目迁移。

与其他方案对比

特性 freezed equatable 手动编写
== / hashCode 自动生成 自动生成 手动编写
copyWith 自动生成(支持深拷贝) 不支持 手动编写
toString 自动生成 手动编写 手动编写
Union Types 支持 不支持 手动编写
JSON 序列化 配合 json_serializable 不支持 手动编写
不可变性 @freezed 默认不可变 不强制 手动保证
代码生成 需要 build_runner 不需要 不需要

freezed 的缺点在于每次修改 Model 后都需要重新运行代码生成,开发时建议使用 watch 模式

dart run build_runner watch -d