面试备战 Flutter 24:Freezed、copyWith、Union 与 build_runner
面试备战 Flutter 24:Freezed、copyWith、Union 与 build_runner
Freezed 面试不是问“会不会写注解”,而是在问你是否理解不可变数据建模、值相等、状态合法性和代码生成边界。
一句话结论:
手写 Model 最大的问题不是麻烦,而是容易不一致。Freezed 用代码生成保证不可变字段、值相等、copyWith、toString、JSON 和 Union 状态的一致性;build_runner 则负责扫描源码、执行 builder、生成
.freezed.dart和.g.dart。
1. 为什么不用手写 Model?
一个靠谱的 Model 不只是字段和构造函数。
通常要写:
- 构造函数。
final字段。toString。operator ==。hashCode。copyWith。fromJson。toJson。- 默认值。
- 可空字段处理。
- 嵌套对象 copy。
- sealed/union 状态。
字段一多,手写就容易漏。
比如新增一个字段 avatar,你可能忘了:
==没加。hashCode没加。copyWith没加。fromJson没加。toJson没加。
这类问题编译器不一定能帮你发现,但线上会变成缓存错误、UI 不刷新、序列化丢字段。
2. Freezed 生成什么?
典型写法:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
abstract class User with _$User {
const factory User({
required String id,
required String name,
(false) bool isVip,
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}
Freezed 会生成:
- 不可变字段实现。
copyWith。toString。==。hashCode。- JSON 入口配合 json_serializable。
- 集合不可变包装。
- Union 类型支持。
3. 为什么 immutable Model 重要?
不可变 Model 的好处:
- 状态变化更可追踪。
- 值相等更可靠。
- 可以安全缓存。
- Riverpod family 参数更稳定。
- Widget diff 和状态比较更简单。
- 避免“对象引用没变但内部字段变了”的刷新问题。
错误倾向:
user.name = 'new name';
notifyListeners();
更推荐:
state = state.copyWith(name: 'new name');
这样状态变化是一次明确的新对象替换。
4. copyWith 怎么实现?
最简单的手写版本:
class User {
const User({
required this.id,
required this.name,
});
final String id;
final String name;
User copyWith({
String? id,
String? name,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
);
}
}
这个版本对非空字段可用,但遇到可空字段会出问题。
5. copyWith 如何处理 null?
假设:
class User {
const User({
required this.id,
this.nickname,
});
final String id;
final String? nickname;
}
你需要表达两件事:
没传 nickname -> 保留旧值
传 nickname: null -> 清空旧值
普通参数:
User copyWith({String? nickname})
无法区分“没传”和“传 null”。
Freezed 会用内部哨兵值处理:
参数未传 -> sentinel -> 使用旧值
参数传 null -> 值就是 null -> 覆盖旧值
这就是为什么 Freezed 的 copyWith(age: null) 可以正确把可空字段设为 null。
6. Union 是什么?
Union 表示一个类型有多个互斥形态。
页面异步状态最典型:
loading | data | error
不用 Union 时,很多人会写:
class PageState {
const PageState({
required this.loading,
this.data,
this.error,
});
final bool loading;
final User? data;
final Object? error;
}
这个模型会允许非法状态:
loading = true
data != null
error != null
Union 让非法状态不可表示:
sealed class UserState with _$UserState {
const factory UserState.loading() = UserLoading;
const factory UserState.data(User user) = UserData;
const factory UserState.error(Object error) = UserError;
}
消费:
switch (state) {
case UserLoading():
return const CircularProgressIndicator();
case UserData(:final user):
return Text(user.name);
case UserError(:final error):
return Text('$error');
}
7. Union 和 Dart sealed class 的关系
Dart 3 提供了 sealed class 和 pattern matching。Freezed 可以和这些语言能力结合:
sealed限制子类集合。switch可以做穷尽检查。- Freezed 生成样板代码。
- 多构造函数表达不同 case。
所以现在更推荐使用 Dart 原生 pattern matching 消费 Union,而不是老版本的 when/map 风格。
8. JSON 怎么配合?
Freezed 本身负责数据类样板;JSON 通常由 json_serializable 生成。
需要:
part 'user.g.dart';
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
构建后会生成:
user.freezed.dart
user.g.dart
常见注意点:
part文件名必须和源文件匹配。fromJson参数类型建议用Map<String, Object?>。- 默认值用
@Default。 - 字段重命名用
@JsonKey(name: 'xxx')。 - 嵌套对象也要支持 fromJson。
9. build_runner 做了什么?
build_runner 是 Dart 生态的代码生成执行器。
它的流程:
读取 package graph
-> 扫描输入文件
-> 找到 builder 配置
-> 分析注解和源码
-> 生成中间资产和输出文件
-> 维护增量缓存
常用命令:
dart run build_runner build --delete-conflicting-outputs
监听:
dart run build_runner watch --delete-conflicting-outputs
Flutter 项目也常用:
flutter pub run build_runner build --delete-conflicting-outputs
10. 为什么生成文件不要手改?
.freezed.dart 和 .g.dart 是构建产物。
手改的问题:
- 下次生成会覆盖。
- 逻辑和源文件不一致。
- code review 噪音大。
- 难以定位真实源头。
应该改源文件和注解,再重新生成。
11. 常见错误
忘记 part
part 'user.freezed.dart';
part 'user.g.dart';
文件名不一致
user_model.dart 对应:
part 'user_model.freezed.dart';
part 'user_model.g.dart';
依赖版本不匹配
需要同时关注:
freezedfreezed_annotationjson_serializablejson_annotationbuild_runner
有冲突输出
使用:
dart run build_runner build --delete-conflicting-outputs
可空 copyWith 手写错误
可空字段复杂时,不要自己用 ?? 硬写。
12. 什么时候不用 Freezed?
Freezed 不是所有地方都必须用。
可以不用的场景:
- 很简单的一次性 DTO。
- 不需要值相等。
- 不需要 copyWith。
- 不需要 JSON。
- 使用 Dart record 足够。
- 团队不想引入代码生成。
但对于核心业务 Model、复杂状态、缓存 key、接口 DTO,Freezed 的一致性收益通常大于生成成本。
13. 面试怎么回答?
可以这样答:
我不用手写 Model 的主要原因是可靠性。一个 Model 不只是字段,还要维护
==、hashCode、copyWith、JSON、默认值和可空字段。字段变多后手写很容易漏。Freezed 用代码生成保证不可变数据类和值相等的一致性,还能生成 copyWith 和 Union 状态。copyWith 的难点是可空字段。手写
param ?? oldValue无法区分“没传”和“传 null”,Freezed 用内部哨兵值解决这个问题。Union 的价值是让非法状态不可表示,比如 loading/data/error 三种状态互斥。配合 Dart sealed class 和 switch pattern,可以让编译器检查所有状态是否处理完整。
build_runner 是代码生成执行器,负责扫描注解、执行 builder、生成
.freezed.dart和.g.dart。生成文件不手改,改源文件和注解后重新生成。
参考资料
- Freezed package: https://pub.dev/packages/freezed
- build_runner: https://dart.dev/tools/build_runner
- Dart class modifiers: https://dart.dev/language/class-modifiers
- Dart patterns: https://dart.dev/language/patterns
- json_serializable: https://pub.dev/packages/json_serializable