Dart: Dart 代码生成 — build_runner 与序列化自动化
代码生成是消灭样板代码的终极武器 — 让机器写代码,让人写逻辑。
1. 你将学到
- build_runner 工作原理:Builder / Generator / AssetReader
- 常用生成器:json_serializable / freezed / dart_mappable
- Part 文件机制:.g.dart / .freezed.dart
- 自定义 Builder 开发入门
- Bob 场景:DataPipeline 用 json_serializable 自动生成序列化代码
2. 一个开发者的真实故事
(1) 痛点:20 个模型类手动序列化花了 3 天
Bob 的 DataPipeline 有 20 个数据模型类,每个都需要 fromJson/toJson 方法。手动编写 20 个类的序列化代码花了 3 天,期间出了 4 次拼写错误和 2 次类型转换错误。更糟的是,每次新增字段都要手动更新 3 处代码(字段声明、fromJson、toJson),一次漏改导致 100,000 条记录解析失败。
(2) json_serializable 的解法
用 @JsonSerializable 注解标注模型类,build_runner 自动生成 fromJson/toJson。新增字段只需声明 + 重新 build,不会再遗漏。
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';
@JsonSerializable()
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
(3) 收益
- 20 个类的序列化代码从 3 天降到 30 分钟
- 新增字段只需改一处,生成器自动更新
- 类型转换错误在编译时就能发现
3. build_runner 工作原理
(1) 生成流水线
flowchart LR
A["model.dart<br/>@JsonSerializable"] --> B[build_runner]
B --> C["model.g.dart<br/>fromJson/toJson"]
B --> D["model.freezed.dart<br/>copyWith/equals"]
A --> E[".part directive"]
E --> C
subgraph Generation Pipeline
B
C
D
end
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 组件 | 职责 |
|---|---|
| Builder | 读取源文件,决定生成什么 |
- Generator | 具体生成代码的逻辑 |
| AssetReader | 读取源代码文件 |
| AssetWriter | 写入生成的文件 |
| Part 文件 |
.g.dart/.freezed.dart|
4. json_serializable
(1) 配置与使用
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:pubspec.yaml 配置
dependencies:
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.7.0
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:基本模型类
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';
@JsonSerializable()
class Order {
final String id;
final double amount;
final String status;
final String? category;
Order({
required this.id,
required this.amount,
required this.status,
this.category,
});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
// Run: dart run build_runner build
// Generates order.g.dart with _$OrderFromJson and _$OrderToJson
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:自定义字段映射
import 'package:json_annotation/json_annotation.dart';
part 'product.g.dart';
@JsonSerializable()
class Product {
@JsonKey(name: 'product_id')
final String id;
@JsonKey(name: 'product_name')
final String name;
@JsonKey(name: 'unit_price')
final double price;
@JsonKey(defaultValue: 'General')
final String category;
@JsonKey(fromJson: _dateTimeFromEpoch, toJson: _dateTimeToEpoch)
final DateTime createdAt;
@JsonKey(ignore: true)
final String? cachedData;
Product({
required this.id,
required this.name,
required this.price,
this.category = 'General',
required this.createdAt,
this.cachedData,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
DateTime _dateTimeFromEpoch(int epoch) => DateTime.fromMillisecondsSinceEpoch(epoch);
int _dateTimeToEpoch(DateTime dt) => dt.millisecondsSinceEpoch;
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:嵌套对象序列化
import 'package:json_annotation/json_annotation.dart';
part 'customer.g.dart';
@JsonSerializable()
class Address {
final String city;
final String? state;
final String country;
Address({required this.city, this.state, required this.country});
factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
@JsonSerializable()
class Customer {
final String name;
final String email;
final Address? address;
Customer({required this.name, required this.email, this.address});
factory Customer.fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| @JsonKey 参数 | 含义 | 示例 |
|---|---|---|
name |
JSON 中的键名 | @JsonKey(name: 'product_id') |
defaultValue |
缺失时的默认值 | @JsonKey(defaultValue: 'N/A') |
fromJson |
自定义反序列化函数 | @JsonKey(fromJson: _parse) |
toJson |
自定义序列化函数 | @JsonKey(toJson: _format) |
ignore |
忽略该字段 | @JsonKey(ignore: true) |
5. freezed
(1) 不可变数据类生成
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:freezed 基本用法
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'order_event.freezed.dart';
part 'order_event.g.dart';
@freezed
class OrderEvent with _$OrderEvent {
const factory OrderEvent.created({
required String orderId,
required double amount,
required DateTime timestamp,
}) = OrderCreated;
const factory OrderEvent.statusChanged({
required String orderId,
required String from,
required String to,
}) = OrderStatusChanged;
const factory OrderEvent.cancelled({
required String orderId,
required String reason,
}) = OrderCancelled;
factory OrderEvent.fromJson(Map<String, dynamic> json) =>
_$OrderEventFromJson(json);
}
// Run: dart run build_runner build
// Generates:
// - order_event.freezed.dart: copyWith, ==, hashCode, toString, pattern matching
// - order_event.g.dart: fromJson, toJson
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| freezed 生成 | 功能 |
|---|---|
copyWith() |
不可变副本 |
== / hashCode |
值相等 |
toString() |
格式化输出 |
when() |
模式匹配回调 |
maybeWhen() |
可选模式匹配 |
fromJson/toJson |
JSON 序列化 |
6. build_runner 命令
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:常用命令
# One-time build
dart run build_runner build
# Watch mode - rebuild on file changes
dart run build_runner watch
# Clean generated files
dart run build_runner clean
# Build with deletion of stale files
dart run build_runner build --delete-conflicting-outputs
# Watch with deletion
dart run build_runner watch --delete-conflicting-outputs
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 命令 | 用途 | 开发阶段 |
|---|---|---|
build |
一次性生成 | 发布 |
watch |
监听变化自动生成 | 开发 |
clean |
清除生成文件 | 重置 |
--delete-conflicting-outputs |
自动覆盖冲突 | 调试 |
7. Part 文件机制
(1) part 与 part of
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:Part 文件关系
// lib/models/order.dart (source file)
import 'package:json_annotation/json_annotation.dart';
// Declare part files
part 'order.g.dart'; // Generated by json_serializable
part 'order.freezed.dart'; // Generated by freezed (if used)
@JsonSerializable()
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
// Generated: order.g.dart
// part of 'order.dart';
// Order _$OrderFromJson(Map<String, dynamic> json) => Order(...)
// Map<String, dynamic> _$OrderToJson(Order instance) => {...}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 指令 | 位置 | 含义 |
|---|---|---|
part 'file.dart' |
源文件 | 声明 part 文件 |
part of 'file.dart' |
生成文件 | 属于哪个源文件 |
.g.dart |
生成文件 | json_serializable |
.freezed.dart |
生成文件 | freezed |
8. 自定义 Builder 入门
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:简单 Builder 概念
// Custom builder concept (simplified)
// In a real project, this would be in a separate package
// A custom annotation
class CsvMapping {
final String columnName;
final bool required;
const CsvMapping({required this.columnName, this.required = true});
}
// Model using the annotation
class Order {
@CsvMapping(columnName: 'order_id')
final String id;
@CsvMapping(columnName: 'total_amount', required: false)
final double amount;
Order({required this.id, required this.amount});
}
// What a custom builder would generate:
// order.mapper.dart
// part of 'order.dart';
//
// Order OrderFromCsv(Map<String, String> row) => Order(
// id: row['order_id'] ?? '',
// amount: double.tryParse(row['total_amount'] ?? '0') ?? 0,
// );
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
9. Bob 场景:DataPipeline 序列化代码生成
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:完整模型定义
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';
@JsonSerializable(createToJson: true, createFactory: true)
class Order {
@JsonKey(name: 'order_id')
final String id;
@JsonKey(name: 'total_amount')
final double amount;
@JsonKey(name: 'order_status', defaultValue: 'pending')
final String status;
@JsonKey(name: 'product_category', required: false)
final String? category;
@JsonKey(name: 'discount_rate', defaultValue: 0)
final double discountRate;
@JsonKey(name: 'created_at')
final DateTime createdAt;
Order({
required this.id,
required this.amount,
this.status = 'pending',
this.category,
this.discountRate = 0,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
// Generated by build_runner
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
// Computed properties (not in JSON)
double get tax => amount * 0.08;
double get total => amount * (1 - 0.08) * (1 - discountRate);
String get formatAmount => '\$${amount.toStringAsFixed(2)} USD';
}
// After running: dart run build_runner build
// The order.g.dart file is generated with:
// - _$OrderFromJson: parses JSON to Order
// - _$OrderToJson: serializes Order to JSON
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
10. 完整示例:DataPipeline 模型序列化
// ============================================
// DataPipeline Model Serialization
// Complete models with json_serializable
// ============================================
import 'package:json_annotation/json_annotation.dart';
part 'models.g.dart';
// Order model
@JsonSerializable()
class Order {
@JsonKey(name: 'order_id')
final String id;
@JsonKey(name: 'total_amount')
final double amount;
@JsonKey(defaultValue: 'pending')
final String status;
@JsonKey(name: 'category')
final String? category;
@JsonKey(name: 'discount_rate', defaultValue: 0.0)
final double discountRate;
@JsonKey(name: 'region', defaultValue: 'US')
final String region;
Order({
required this.id,
required this.amount,
this.status = 'pending',
this.category,
this.discountRate = 0.0,
this.region = 'US',
});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
double get effectiveAmount => amount * (1 - discountRate);
double get tax => effectiveAmount * 0.08;
double get total => effectiveAmount + tax;
String get formatTotal => '\$${total.toStringAsFixed(2)} USD';
}
// Report model
@JsonSerializable()
class Report {
final String title;
final int totalOrders;
final int completedOrders;
final double totalRevenue;
final double totalTax;
final Map<String, double> revenueByCategory;
final DateTime generatedAt;
Report({
required this.title,
required this.totalOrders,
required this.completedOrders,
required this.totalRevenue,
required this.totalTax,
required this.revenueByCategory,
DateTime? generatedAt,
}) : generatedAt = generatedAt ?? DateTime.now();
factory Report.fromJson(Map<String, dynamic> json) => _$ReportFromJson(json);
Map<String, dynamic> toJson() => _$ReportToJson(this);
String get formatRevenue => '\$${totalRevenue.toStringAsFixed(2)} USD';
double get averageOrderValue => completedOrders > 0 ? totalRevenue / completedOrders : 0;
}
// Simulated usage (in production, .g.dart would be generated)
void main() {
// Simulating what the generated code would do
final json = {
'order_id': 'ORD-001',
'total_amount': 1500.0,
'status': 'completed',
'category': 'Electronics',
'discount_rate': 0.1,
'region': 'US',
};
// Manual parsing (simulating _$OrderFromJson)
final order = Order(
id: json['order_id'] as String,
amount: (json['total_amount'] as num).toDouble(),
status: json['status'] as String? ?? 'pending',
category: json['category'] as String?,
discountRate: (json['discount_rate'] as num?)?.toDouble() ?? 0.0,
region: json['region'] as String? ?? 'US',
);
print('=== DataPipeline Order ===');
print('ID: ${order.id}');
print('Amount: \$${order.amount.toStringAsFixed(2)} USD');
print('Discount: ${(order.discountRate * 100).toStringAsFixed(0)}%');
print('Tax: \$${order.tax.toStringAsFixed(2)} USD');
print('Total: ${order.formatTotal}');
print('Category: ${order.category ?? "N/A"}');
print('Region: ${order.region}');
// Simulate report generation
final report = Report(
title: 'Daily Analytics Report',
totalOrders: 1000,
completedOrders: 850,
totalRevenue: 525000.0,
totalTax: 42000.0,
revenueByCategory: {
'Electronics': 360000.0,
'Clothing': 89000.0,
'Books': 76000.0,
},
);
print('\n=== Report ===');
print('Title: ${report.title}');
print('Orders: ${report.completedOrders}/${report.totalOrders}');
print('Revenue: ${report.formatRevenue}');
print('Average: \$${report.averageOrderValue.toStringAsFixed(2)} USD');
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
输出:
=== DataPipeline Order ===
ID: ORD-001
Amount: $1500.00 USD
Discount: 10%
Tax: $108.00 USD
Total: $1458.00 USD
Category: Electronics
Region: US
=== Report ===
Title: Daily Analytics Report
Orders: 850/1000
Revenue: $525000.00 USD
Average: $617.65 USD
❓ 常见问题
Q:build_runner watch 模式会很慢吗? A:首次构建需要扫描所有文件,较慢(10-30 秒)。后续增量构建只处理修改的文件,通常 1-3 秒。大项目可用
--build-filter只生成指定文件。
Q:json_serializable 和手写 fromJson 性能有差异吗? A:几乎没有。生成的代码与手写代码质量相当,甚至在某些场景下更优(使用更精确的类型检查)。
Q:freezed 和 json_serializable 必须一起用吗? A:不是。json_serializable 负责序列化,freezed 负责不可变类生成。可以只用 json_serializable,也可以两者组合使用。
Q:生成文件需要提交到版本控制吗? A:应用项目建议提交(确保 CI 不需要 build_runner);库项目建议提交(pub.dev 发布需要)。也有一些团队选择 .gitignore 生成文件。
Q:如何调试生成的代码? A:直接打开 .g.dart 文件阅读。生成的代码是标准 Dart 代码,可以设断点、加 print。问题通常在注解配置上,不在生成逻辑。
Q:build_runner 和 source_gen 有什么关系? A:source_gen 是 build_runner 的上层抽象,提供更简单的 API 来编写自定义 Builder。json_serializable 和 freezed 都基于 source_gen。
Q:可以自定义 @JsonKey 的 fromJson/toJson 类型吗? A:可以。定义顶层函数或静态方法,签名需匹配
T fromJson(Object? json)和Object toJson(T value)。在 @JsonKey 中引用即可。
📖 小节
- build_runner 是 Dart 代码生成的执行引擎,读取注解 → 生成代码
- json_serializable 自动生成 fromJson/toJson,@JsonKey 自定义映射
- freezed 生成不可变数据类:copyWith、==、hashCode、when
- Part 文件机制将生成代码与源代码关联
- DataPipeline 用 json_serializable 消除 20 个模型类的手写序列化
📝 作业
- 基础题(难度⭐):创建一个 Dart 项目,添加 json_serializable 和 build_runner 依赖。为一个简单的 Product 类(3 个字段)添加 @JsonSerializable 注解,运行
dart run build_runner build,查看生成的 .g.dart 文件。 - 进阶题(难度⭐⭐):为一个嵌套模型(Order 包含 List
<Product>,Product 包含 Category 枚举)配置 json_serializable,处理自定义字段名和默认值。验证 fromJson/toJson 的往返正确性。 - 挑战题(难度⭐⭐⭐):结合 freezed 和 json_serializable,为 DataPipeline 创建一个 Sealed Class 风格的 OrderEvent 模型(Created/StatusChanged/Cancelled),生成不可变类 + JSON 序列化 + copyWith。编写测试验证生成代码。