Skip to content

build_runner 代码生成

build_runner 是 Dart 官方提供的代码生成工具运行器,它本身不生成代码,而是驱动其他代码生成包(如 json_serializablefreezed 等)自动产出 .g.dart / .freezed.dart 文件,免去手写样板代码的繁琐。

为什么需要 build_runner?

在 Flutter 开发中,很多重复性代码可以用工具自动生成:

场景手写方式build_runner + 生成包
JSON 序列化手写 fromJson / toJsonjson_serializable 自动生成
不可变数据类手写 copyWith==hashCodefreezed 自动生成
路由定义手动维护路由表go_router_builder 自动生成
Mock 测试手写 Mock 类mockito 自动生成
国际化手动维护字符串映射slang 等自动生成

核心理念

你只写「声明」,工具帮你写「实现」。比如你用注解声明一个类需要 JSON 序列化,build_runner 就会帮你生成对应的 fromJson / toJson 方法。

安装

build_runner 是开发依赖,放在 dev_dependencies 中:

yaml
# pubspec.yaml
dev_dependencies:
  build_runner: ^2.4.0

同时,你需要安装具体的代码生成包。以下是常见组合:

yaml
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_serializablefreezed)必须放在 dev_dependencies 中,它们只在开发时使用,不会打包进最终应用。

核心命令

一次性构建

bash
dart run build_runner build

扫描项目中所有带注解的文件,生成对应代码。适合在 CI/CD 或偶尔需要生成时使用。

监听模式(推荐开发时使用)

bash
dart run build_runner watch

启动后会持续监听文件变化,文件保存即自动重新生成,开发体验更流畅。按 Ctrl + C 退出。

清理生成文件

bash
dart run build_runner clean

删除所有生成的文件(.g.dart.freezed.dart 等)。当你遇到生成异常或缓存问题时使用。

删除再重建

bash
dart run build_runner build --delete-conflicting-outputs

当生成文件与现有文件冲突时,加 --delete-conflicting-outputs 可自动覆盖冲突文件。这是最常用的构建命令

命令速查

命令用途使用场景
build一次性生成所有代码CI/CD、首次生成
watch监听变化持续生成日常开发首选
clean清理所有生成文件生成异常、切换分支后
build --delete-conflicting-outputs强制覆盖冲突重建生成冲突时必用

实战:JSON 序列化

这是 build_runner 最常见的用途。以一个用户模型为例:

1. 定义模型

dart
// 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. 运行生成

bash
dart run build_runner build --delete-conflicting-outputs

3. 使用生成的代码

dart
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. 嵌套模型

dart
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==hashCodetoString 等样板代码。

1. 定义数据类

dart
// 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. 运行生成

bash
dart run build_runner build --delete-conflicting-outputs

3. 使用

dart
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

dart
// user.dart
part 'user.g.dart';  // 声明:user.g.dart 是我的一部分

// user.g.dart(自动生成)
part of 'user.dart'; // 声明:我属于 user.dart

part 机制让两个文件共享同一个库作用域,生成的代码可以直接访问源文件中的私有成员。这是 Dart 代码生成的基础机制。

常见注解速查

json_serializable 常用注解

dart
@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 类级别配置

dart
@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 完整配置模板

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」

生成文件可能过期或缺失,重新运行:

bash
dart run build_runner build --delete-conflicting-outputs

watch 模式卡住不生成

  1. Ctrl + C 停止 watch
  2. 运行 dart run build_runner clean
  3. 重新 dart run build_runner watch

生成文件冲突

[SEVERE] Conflicting outputs were detected

--delete-conflicting-outputs 参数即可:

bash
dart run build_runner build --delete-conflicting-outputs

切换 Git 分支后编译报错

不同分支的生成文件可能不一致,切换后执行:

bash
dart run build_runner build --delete-conflicting-outputs

生成速度慢

  • 首次生成较慢是正常的,后续增量生成会快很多
  • 使用 watch 模式代替反复 build,只重新生成变化的文件
  • 可以通过 build.yaml 配置只扫描特定目录:
yaml
# build.yaml(放在项目根目录)
targets:
  $default:
    sources:
      include:
        - lib/models/**  # 只扫描 models 目录

下一步

基于 Flutter 官方文档整理