深色模式
Freezed 使用说明
概述
Freezed 不是一个“主要用来做 JSON 序列化”的库,它更准确的定位是 Dart/Flutter 里的代码生成工具,用来生成:
- 不可变数据类
copyWith==、hashCode、toString- union / sealed class
- 配合
json_serializable生成fromJson/toJson
如果你的项目里经常要写模型类、状态类、接口返回类型,Freezed 基本可以把大量重复代码收掉。
安装
Freezed 的基础依赖是这些:
sh
flutter pub add freezed_annotation
flutter pub add dev:freezed
flutter pub add dev:build_runner如果你还需要生成 JSON 序列化代码,再补上:
sh
flutter pub add json_annotation
flutter pub add dev:json_serializable这里可以顺手记住职责:
freezed_annotation:注解定义freezed:代码生成器build_runner:运行生成器json_annotation、json_serializable:JSON 相关生成
生成代码
最常用的生成命令有两个:
sh
dart run build_runner buildsh
dart run build_runner watch -d一般来说:
build适合一次性生成watch -d适合开发时持续监听文件变化
使用 Freezed 时,模型文件通常都要声明 part:
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';如果当前模型不做 JSON 序列化,可以不写 part 'user.g.dart';。
基本模型写法
最常见的 Freezed 写法,是用 @freezed 标注一个模型,然后通过工厂构造函数定义字段:
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
sealed class User with _$User {
const factory User({
required String id,
required String name,
int? age,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}生成后,Freezed 会自动提供这些能力:
- 不可变字段
copyWith- 值相等比较
- 友好的
toString - 配合
json_serializable生成 JSON 方法
如果你只是想定义一个标准数据模型,这种写法通常已经够用。
copyWith
copyWith 是 Freezed 最常用的能力之一,用来基于旧对象生成一个新对象。
dart
final user = User(id: '1', name: 'Remi', age: 24);
final newUser = user.copyWith(name: 'Dash');
final clearedAgeUser = user.copyWith(age: null);它的好处是:
- 不需要自己手写拷贝逻辑
- 可选字段可以显式改成
null - 保持模型不可变
对于嵌套对象,Freezed 还支持更深层的 copyWith 调用:
dart
final company = Company(
name: 'Google',
director: Director(
name: 'Larry',
assistant: Assistant(name: 'John'),
),
);
final updatedCompany = company.copyWith.director.assistant(
name: 'Jane',
);这类深层更新是 Freezed 相比手写模型很省事的一点。
默认值与不可变数据
如果你希望字段有默认值,可以用 @Default:
dart
@freezed
sealed class CounterState with _$CounterState {
const factory CounterState({
@Default(0) int count,
@Default(false) bool loading,
}) = _CounterState;
}这样创建对象时就可以省掉部分参数:
dart
const state = CounterState();Freezed 还会默认把集合做成不可修改视图,所以像下面这种写法通常会报错:
dart
@freezed
sealed class TodoState with _$TodoState {
const factory TodoState({
@Default(<String>[]) List<String> items,
}) = _TodoState;
}dart
final state = TodoState();
state.items.add('todo'); // 运行时不允许直接修改这正是 Freezed 的设计目的之一:鼓励你始终用新的对象表示新的状态,而不是原地改旧对象。
fromJson / toJson
Freezed 本身不直接替代 json_serializable,而是和它配合工作。
最常见的写法是:
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'article.freezed.dart';
part 'article.g.dart';
@freezed
sealed class Article with _$Article {
const factory Article({
required String id,
required String title,
required List<String> tags,
}) = _Article;
factory Article.fromJson(Map<String, dynamic> json) =>
_$ArticleFromJson(json);
}生成后就可以这样使用:
dart
final article = Article.fromJson(json);
final map = article.toJson();如果模型里还有嵌套对象、列表对象,常见做法是给构造函数加上 @JsonSerializable(explicitToJson: true):
dart
@freezed
sealed class Post with _$Post {
@JsonSerializable(explicitToJson: true)
const factory Post({
required String id,
required Author author,
}) = _Post;
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
}如果你只是写普通数据模型,这一套通常就够了。
union / sealed class
Freezed 的另一个核心能力是定义 union,也就是一组互斥的状态类型。
例如接口状态、页面状态、异步状态就很适合这样写:
dart
@freezed
sealed class ApiResult with _$ApiResult {
const factory ApiResult.data(String value) = ApiResultData;
const factory ApiResult.loading() = ApiResultLoading;
const factory ApiResult.error(String message) = ApiResultError;
factory ApiResult.fromJson(Map<String, dynamic> json) =>
_$ApiResultFromJson(json);
}这里有几个关键点:
- 每个构造函数代表一种状态
- 每种状态可以有完全不同的字段
- Freezed 会帮你生成对应的类型和 JSON 分发逻辑
如果多个分支拥有同名字段,那么这个共享字段可以直接从 union 根类型上读取:
dart
@freezed
sealed class Example with _$Example {
const factory Example.person({
required String name,
required int age,
}) = Person;
const factory Example.city({
required String name,
required int population,
}) = City;
}这里 name 是共享字段,但 age 和 population 不是。
Dart 3 模式匹配
在 Dart 3 里,Freezed 官方更推荐直接使用语言内建的模式匹配,而不是依赖旧的 when / map 风格。
比如上面的 ApiResult,可以直接这样读:
dart
String render(ApiResult result) {
return switch (result) {
ApiResultData(:final value) => value,
ApiResultLoading() => 'loading',
ApiResultError(:final message) => 'error: $message',
};
}这类写法的好处是:
- 和 Dart 3 语言本身一致
- 分支更明确
- 处理 union 时更自然
如果你之前习惯的是 when / map,现在更推荐优先学习 switch 模式匹配。
常见配置
Freezed 支持通过注解或项目级配置调整行为,常见的有这些:
@Freezed(...)
你可以通过 @Freezed 调整某个模型的生成行为,比如关闭 copyWith:
dart
@Freezed(copyWith: false)
sealed class Demo with _$Demo {
const factory Demo({
required String name,
}) = _Demo;
}union JSON 配置
如果你的 union 需要自定义 JSON 里的类型字段,可以这样写:
dart
@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.pascal)
sealed class Response with _$Response {
const factory Response.data(String value) = ResponseData;
const factory Response.error(String message) = ResponseError;
factory Response.fromJson(Map<String, dynamic> json) =>
_$ResponseFromJson(json);
}这样生成的 JSON 会通过 type 字段区分具体分支。
集合是否保持不可变
默认情况下,Freezed 会对集合使用不可修改视图。如果你明确想关闭这个行为,也可以配置 makeCollectionsUnmodifiable: false。
不过大多数状态模型并不建议这么做。
IDE 扩展
如果你用 VSCode 或 JetBrains 系列 IDE,官方文档里也提到了对应的 Freezed 扩展,可以提升模板生成和跳转体验。不过它们只是辅助工具,不影响 Freezed 本身的用法。
