build_runner 代码生成
build_runner 是 Dart 官方提供的代码生成工具运行器,它本身不生成代码,而是驱动其他代码生成包(如 json_serializable、freezed 等)自动产出 .g.dart / .freezed.dart 文件,免去手写样板代码的繁琐。
为什么需要 build_runner?
在 Flutter 开发中,很多重复性代码可以用工具自动生成:
| 场景 | 手写方式 | build_runner + 生成包 |
|---|---|---|
| JSON 序列化 | 手写 fromJson / toJson | json_serializable 自动生成 |
| 不可变数据类 | 手写 copyWith、==、hashCode | freezed 自动生成 |
| 路由定义 | 手动维护路由表 | go_router_builder 自动生成 |
| Mock 测试 | 手写 Mock 类 | mockito 自动生成 |
| 国际化 | 手动维护字符串映射 | slang 等自动生成 |
核心理念
你只写「声明」,工具帮你写「实现」。比如你用注解声明一个类需要 JSON 序列化,build_runner 就会帮你生成对应的 fromJson / toJson 方法。
安装
build_runner 是开发依赖,放在 dev_dependencies 中:
# pubspec.yaml
dev_dependencies:
build_runner: ^2.4.0同时,你需要安装具体的代码生成包。以下是常见组合:
dependencies:
# JSON 序列化
json_annotation: ^4.9.0
# 不可变数据类
freezed_annotation: ^2.4.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.7.0 # json_annotation 的生成器
freezed: ^2.4.0 # freezed_annotation 的生成器注意
build_runner 和生成器包(如 json_serializable、freezed)必须放在 dev_dependencies 中,它们只在开发时使用,不会打包进最终应用。
核心命令
一次性构建
dart run build_runner build扫描项目中所有带注解的文件,生成对应代码。适合在 CI/CD 或偶尔需要生成时使用。
监听模式(推荐开发时使用)
dart run build_runner watch启动后会持续监听文件变化,文件保存即自动重新生成,开发体验更流畅。按 Ctrl + C 退出。
清理生成文件
dart run build_runner clean删除所有生成的文件(.g.dart、.freezed.dart 等)。当你遇到生成异常或缓存问题时使用。
删除再重建
dart run build_runner build --delete-conflicting-outputs当生成文件与现有文件冲突时,加 --delete-conflicting-outputs 可自动覆盖冲突文件。这是最常用的构建命令。
命令速查
| 命令 | 用途 | 使用场景 |
|---|---|---|
build | 一次性生成所有代码 | CI/CD、首次生成 |
watch | 监听变化持续生成 | 日常开发首选 |
clean | 清理所有生成文件 | 生成异常、切换分支后 |
build --delete-conflicting-outputs | 强制覆盖冲突重建 | 生成冲突时必用 |
实战:JSON 序列化
这是 build_runner 最常见的用途。以一个用户模型为例:
1. 定义模型
// user.dart
import 'package:json_annotation/json_annotation.dart';
// 关键:part 指令引入即将生成的文件
part 'user.g.dart';
@JsonSerializable() // 注解:告诉生成器这个类需要 JSON 序列化
class User {
final String name;
@JsonKey(name: 'mobile_phone') // 字段名映射:JSON 中叫 mobile_phone
final String phone;
final String? email; // 可空字段
User({required this.name, required this.phone, this.email});
// 工厂方法:从 JSON 创建对象,_$UserFromJson 由生成器生成
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// 转为 JSON,_$UserToJson 由生成器生成
Map<String, dynamic> toJson() => _$UserToJson(this);
}2. 运行生成
dart run build_runner build --delete-conflicting-outputs3. 使用生成的代码
import 'dart:convert';
// JSON 字符串 → 对象
final jsonStr = '{"name": "Tom", "mobile_phone": "13800138000"}';
final user = User.fromJson(json.decode(jsonStr));
print(user.name); // Tom
print(user.phone); // 13800138000
// 对象 → JSON 字符串
final output = json.encode(user.toJson());4. 嵌套模型
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';
@JsonSerializable()
class Order {
final String id;
final User user; // 嵌套对象,User 也必须有 @JsonSerializable
Order({required this.id, required this.user});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}注意
嵌套模型中的每个类都必须标注 @JsonSerializable() 并提供 fromJson / toJson,否则生成器无法处理嵌套关系。
实战:Freezed 不可变数据类
freezed 是另一个常用的代码生成包,能自动生成 copyWith、==、hashCode、toString 等样板代码。
1. 定义数据类
// product.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'product.freezed.dart';
part 'product.g.dart';
@freezed
class Product with _$Product {
const factory Product({
required String id,
required String name,
required double price,
@Default(0) int stock, // 默认值
}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
}2. 运行生成
dart run build_runner build --delete-conflicting-outputs3. 使用
final product = Product(id: '1', name: '手机', price: 3999.0);
// copyWith 创建副本(原对象不可变)
final updated = product.copyWith(price: 3499.0, stock: 100);
// 自动生成的 == 运算符
print(product == updated); // false
// 自动生成的 toJson / fromJson
final json = product.toJson();
final fromJson = Product.fromJson(json);freezed vs 手写
freezed生成一个不可变类,所有字段都是final,修改用copyWith- 自动生成
==和hashCode,可以直接用于 Map 的 key 或 Set - 如果类只需要 JSON 序列化,用
json_serializable就够了;如果还需要不可变性和copyWith,选freezed
文件结构说明
运行 build_runner 后,项目文件结构如下:
lib/
├── models/
│ ├── user.dart ← 你手写的源文件
│ ├── user.g.dart ← build_runner 生成的 JSON 序列化代码
│ ├── product.dart ← 你手写的源文件
│ ├── product.freezed.dart ← freezed 生成的不可变类代码
│ └── product.g.dart ← freezed 的 JSON 序列化代码重要规则
- 永远不要手动编辑
.g.dart和.freezed.dart文件,下次运行build_runner会被覆盖 - 务必将生成文件提交到 Git,这样团队成员不需要重新运行
build_runner part指令中的文件名必须与生成文件名一致(如part 'user.g.dart')
关键概念:part 与 part of
// user.dart
part 'user.g.dart'; // 声明:user.g.dart 是我的一部分
// user.g.dart(自动生成)
part of 'user.dart'; // 声明:我属于 user.dartpart 机制让两个文件共享同一个库作用域,生成的代码可以直接访问源文件中的私有成员。这是 Dart 代码生成的基础机制。
常见注解速查
json_serializable 常用注解
@JsonSerializable() // 标记类需要生成序列化代码
class User {
@JsonKey(name: 'mobile_phone') // JSON 字段名映射
final String phone;
@JsonKey(defaultValue: '') // JSON 中缺省时的默认值
final String avatar;
@JsonKey(ignore: true) // 忽略该字段,不参与序列化
final String temp;
@JsonKey(fromJson: _parseDate) // 自定义解析函数
final DateTime createdAt;
}
DateTime _parseDate(String value) => DateTime.parse(value);json_serializable 类级别配置
@JsonSerializable(
createToJson: true, // 是否生成 toJson(默认 true)
createFactory: true, // 是否生成 fromJson 工厂(默认 true)
explicitToJson: true, // 嵌套对象是否调用其 toJson(默认 false)
fieldRename: FieldRename.snake, // 全局字段重命名策略
)
class Order { ... }explicitToJson
当你需要将嵌套对象 json.encode() 为字符串时,如果嵌套对象没有被正确序列化(输出 Instance of ...),加上 explicitToJson: true 即可解决。
pubspec.yaml 完整配置模板
dependencies:
flutter:
sdk: flutter
# 按需选择:JSON 序列化
json_annotation: ^4.9.0
# 按需选择:不可变数据类
freezed_annotation: ^2.4.0
dev_dependencies:
flutter_test:
sdk: flutter
# 代码生成工具
build_runner: ^2.4.0
# 按需选择:JSON 序列化生成器
json_serializable: ^6.7.0
# 按需选择:不可变数据类生成器
freezed: ^2.4.0常见问题
生成后报错「The getter xxx isn't defined」
生成文件可能过期或缺失,重新运行:
dart run build_runner build --delete-conflicting-outputswatch 模式卡住不生成
- 先
Ctrl + C停止 watch - 运行
dart run build_runner clean - 重新
dart run build_runner watch
生成文件冲突
[SEVERE] Conflicting outputs were detected加 --delete-conflicting-outputs 参数即可:
dart run build_runner build --delete-conflicting-outputs切换 Git 分支后编译报错
不同分支的生成文件可能不一致,切换后执行:
dart run build_runner build --delete-conflicting-outputs生成速度慢
- 首次生成较慢是正常的,后续增量生成会快很多
- 使用
watch模式代替反复build,只重新生成变化的文件 - 可以通过
build.yaml配置只扫描特定目录:
# build.yaml(放在项目根目录)
targets:
$default:
sources:
include:
- lib/models/** # 只扫描 models 目录