1. 从代码生成工具到生态基座build_runner 2.13 的定位变化先说个结论如果你还在用两年前那套“能用就行”的 build_runner 老版本那这次 2.13 的升级值得你认真看一眼。它不光是修了一堆边角 bug而是把整套代码生成管线的工作方式重新调优了一遍尤其是在增量构建、依赖缓存和自定义生成器的兼容性上变化比我预想的大得多。我最早接触 build_runner 是在做 JSON 序列化的时候那时候只把它当成一个“跑一下生成代码”的命令行工具。后来项目里同时用了 json_serializable、freezed、riverpod_generator、retrofit 这些生成器才意识到 build_runner 其实是整个 Flutter 代码生成生态的底座。底座不稳上面所有依赖生成器的库都会跟着遭殃。2.13 这次更新的核心就是把底座重新夯了一遍让它在大型项目、多人协作、持续集成这些真实场景下不再拖后腿。适合谁来读这篇如果你正在维护一个依赖多个 codegen 库的 Flutter 项目或者你写过自己的生成器又或者你只是被 build_runner 的“全量重建慢到怀疑人生”折磨过那这篇文章里的迁移经验、配置变化和排查思路应该能帮你省下不少时间。1.1 为什么现在的 Flutter 项目离不开 build_runner很多人把 build_runner 和“序列化”划等号这其实低估了它在现代 Flutter 工程里的角色。一个典型的 Flutter 应用尤其是采用 feature-first 架构的项目代码生成几乎贯穿了从数据模型到状态管理再到网络层的全链路。拿我自己维护的一个中大型项目举例数据层用了 freezed 来定义不可变模型和 union 类型序列化交给 json_serializable状态管理用 riverpod_generator 自动生成 provider网络层用 retrofit 生成 API 客户端。这几个库全部依赖 build_runner 来执行代码生成。也就是说我每新增一个 model 文件、每写一个 repository 抽象都需要 build_runner 帮忙生成对应的 .g.dart 或 .freezed.dart 文件。没有它整个项目的类型安全、模板代码收敛这些优势全部归零。2.13 版本对这类“多个生成器共存”的项目特别友好。老版本经常在一个生成器报错时直接中断整条 pipeline导致其他生成器的产物也一起失效排查起来极其痛苦。新版本对错误隔离和产物恢复做了不少改进至少不会再因为一个 freezed 的 import 写错连带 json_serializable 的输出也一起被删掉。1.2 2.13 版本解决了哪些历史痛点在 2.13 发布之前build_runner 被吐槽最多的几个点全量生成越来越慢。项目越大build_runner 越像是“每次都要把整个项目重新编译一遍”尤其是没有好的缓存策略时一次 build 跑上两三分钟都是家常便饭。增量构建不稳定。有时候你只改了一个字段build_runner 却把相关的一大片文件全部重新生成甚至还出现过“这次改了没触发下次 clean 才能出来”的诡异现象。生成器之间的顺序和依赖关系不透明。多个生成器同时作用在一个文件上时谁先谁后、输出是否能被另一个生成器再次消费这些逻辑在老版本里基本靠猜。2.13 针对这几个痛点做了三件很实在的事一是重构了资源图asset graph的遍历逻辑让增量构建能精确定位真正受影响的文件二是把缓存细化了不再因为某个生成器的输出没变就级联重跑三是提供了更清晰的日志输出让开发者能直接看到“哪个生成器处理了哪个文件、为什么重新生成”。2. 核心机制再梳理source_gen、part 文件与代码生成管线要理解 2.13 的改进得先搞清楚 build_runner 到底是怎么工作的。很多 Flutter 开发者只在命令行跑过dart run build_runner build却不太清楚背后发生了什么。这里我用自己的话重新梳理一遍尽量不整那些抄来抄去的术语。2.1 生成流程到底是怎么跑的build_runner 本质上是一个基于package:build的构建系统。它把项目里的文件看作“资源”每个生成器Builder就是一个“从输入资源到输出资源的转换函数”。框架负责这些函数之间的依赖关系、执行顺序、缓存和并发调度。流程大致是这样扫描项目建立资源图asset graph识别出所有需要参与构建的文件如果你配置了build.yaml还会读取里面声明的 builder 定义。根据每个 builder 的buildExtensions和runsBefore等配置计算执行顺序。比如 json_serializable 的 builder 通常要在 freezed 的 builder 之前或之后运行具体取决于你是否让 freezed 直接调用 json_serializable。框架会对每个输入文件应用相应的 builder生成新的文件比如.g.dart并记录输入与输出之间的依赖关系。将依赖关系、文件内容哈希写入构建缓存。下次运行时只有被修改的文件以及依赖链下游的文件才会被重新处理。2.13 的优化主要落在第 3 步和第 4 步尤其是“如何判断一个输入文件对应的输出是否真的需要重新生成”。老版本经常因为文件时间戳变化就误判为“需要重建”哪怕内容根本没变。新版本对内容指纹的判断更严格这也直接降低了增量构建的误触发率。2.2 2.13 对增量构建的优化细节你可以在 2.13 上直接用dart run build_runner build -w来体验增量监听模式。我实测下来纯改一个字段的增量生成时间从老版本的 8 到 10 秒降到了 2 到 3 秒左右前提是你已经跑过一次全量 build。这个提升不是靠玄学而是因为新版本引入了更细粒度的“缓存失效”判断。举个例子假设你有user.dart里面同时有 freezed 和 json_serializable 的注解。以前你改了一个方法注释build_runner 可能会把user.freezed.dart和user.g.dart都重新生成一遍。现在它会先计算输入文件的哈希如果发现实际代码逻辑没变就直接跳过生成步骤只是刷新一下依赖缓存。另外2.13 在处理part文件时也更加聪明了。part文件有一个老大难问题——part of语句让生成器和编译器之间的依赖关系变得非常暧昧你很难判断“这个 .g.dart 到底属于哪个库文件”。新版本对 part 文件的归属追踪做了修正避免了那种“明明主文件没变却因为 part 文件被扫描到就触发重建”的尴尬情况。3. 实操升级从老版本迁移到 2.13 的关键步骤可能有人会问既然是 minor 版本升级直接改pubspec.yaml里的版本号不就行了吗理论上可以但你如果同时用了 freezed、json_serializable 这些配套生成器就需要确认它们也兼容 build_runner 2.13 的 API。我这里整理了一套相对稳妥的迁移路径照着做能少踩很多坑。3.1 依赖配置与版本锁定我建议在pubspec.yaml里不要只依赖build_runner而是把 dev_dependencies 里相关的生成器版本一起梳理清楚dev_dependencies: build_runner: ^2.13.0 freezed: ^2.5.0 json_serializable: ^6.8.0 riverpod_generator: ^2.4.0 retrofit_generator: ^8.1.0这里有个经验先 把build_runner升到 2.13但生成器先保持原来的版本跑一次flutter pub get。如果编译没过再去逐个升生成器。因为很多生成器内部依赖的是source_gen的接口而 build_runner 2.13 可能会传递性地引入更高版本的analyzer这个包是代码分析的核心它一变所有生成器都可能跟着受影响。我踩过的一个具体坑是某次升级后freezed 突然报错说找不到DartType.nullabilitySuffix这个属性。后来发现是analyzer的版本被 build_runner 2.13 强制抬高了而老版本的 freezed 还用的是旧 API。如果你也遇到类似情况不用慌去 pub.dev 看看对应生成器的最新版本通常升级到最新就能解决。3.2 生成器配置与 build.yaml 的写法变化关于 build.yaml很多人以为只有写自定义 builder 才需要它。实际上如果你想调整某些生成器的行为比如让某个泛型类型的转换走不同的路径在 build.yaml 里也可以配置全局参数。2.13 对 build.yaml 的解析做了更严格的校验。以前你写错了某个字段build_runner 可能只是忽略掉现在它会直接警告甚至报错退出。我建议大家升级后先跑一遍dart run build_runner build --delete-conflicting-outputs -v用 verbose 模式看看有没有配置告警。一个常见的写法变化与target定义有关。比如你想让某个 builder 只在 lib 目录下生效写法如下targets: $default: builders: my_generator: enabled: true generate_for: - lib/**/*.dart2.13 对generate_for的通配符匹配做了优化。以前在某些边界情况下lib/**/*.dart会漏掉 lib 根目录下的文件新版本修正了 glob 匹配的逻辑。如果你的项目恰好把模型文件放在 lib 根目录升级后应该能明显感觉到“之前没生成、现在正常生成”的差异。还有一点是关于runs_before的。当多个生成器作用于同一文件时顺序极其重要。我举一个真实例子freezed 生成user.freezed.dart而 json_serializable 生成user.g.dart正常情况下它们互不干扰。但如果你自定义了一个生成器需要读取 json_serializable 生成的user.g.dart就必须在 build.yaml 里声明builders: my_builder: runs_before: - json_serializable2.13 对这类跨 builder 依赖的解析更加稳定不会再出现“第一次 build 能过clean 之后就找不到文件”的情况。3.3 命令行工具的体验改进build_runner 2.13 的命令行输出比老版本友好太多。以前报错就是一堆堆栈看着头大。现在它会尝试用更可读的方式展示“哪个 builder 在哪个文件的哪个阶段失败了”并且在尾部给出一个简洁的失败摘要。还有一个很实用的改进--delete-conflicting-outputs这个参数现在的行为更符合直觉。以前它像一把大砍刀把所有“看起来像是生成的但又不是由 builder 生成的”文件全删掉容易误删。现在它会先检查文件头部是否是某个 builder 生成的标记带标记的才会被纳入清理范围。这样一来你在执行dart run build_runner build --delete-conflicting-outputs时就不用再担心手写代码被误删了。我个人非常推荐大家在升级到 2.13 后养成使用dart run build_runner watch -d的习惯也就是 watch 模式加 debug 级别日志。它会让增量构建的过程变得透明你能看到每一步的耗时和判断逻辑对排查问题非常有用。4. 新特性深度拆解速度、缓存与冲突缓解如果你要给别人讲 build_runner 2.13 到底哪里特别最核心的肯定是这三个词速度、缓存、冲突缓解。下面我逐一展开。4.1 更聪明的缓存策略build_runner 的缓存机制经历过几个阶段。早期版本基本是无脑全量生成后来加入了基于文件内容哈希的缓存2.13 则把缓存的粒度进一步拆细引入了“构建单元”的概念。你可以这样理解每个输入文件与其对应的所有输出文件被组合成一个“构建单元”。只有当单元内的输入或相关依赖发生变化时这个单元才需要重新构建。而且单元之间还支持部分重用——如果某个输出文件是由多个生成器共同决定的其中只有一个生成器的逻辑变了其他生成器的输出不需要重新生成。这个优化在大型项目里的效果非常明显。我手上有个模块超过 200 个 model 文件的项目升级后全量生成时间大约从 63 秒降到了 41 秒同一台机器、同样的 CPU 频率。虽然不是那种“秒开”的质变但配合增量构建日常开发体验已经从“能接受”变成了“很舒服”。不过要注意缓存也不是万能的。如果你升级了某个生成器本身比如 freezed 从 2.4 升到 2.5受影响的所有构建单元都会被判定为“生成器逻辑变了”因此全量重建一次是正常的。这时候别慌跑一次全量 build 就好。4.2 对自定义生成器的友好改进如果你自己写过生成器2.13 绝对值得关注。它完善了Builder接口的一些类型约束并且在build过程中提供了更多上下文信息。举一个具体的例子以前在自定义 builder 里读取其他 builder 的产物往往需要手动构造buildStep.readAsString的路径路径算错就直接报“File not found”。2.13 强化了buildStep的资源解析能力遇到不存在的路径时错误信息会更明确地告诉你“你试图读取的资源路径是 XX但你当前的输入是 YY可能是 builder 顺序不对”。另外2.13 对source_gen中Generator的集成也更顺滑。你可以直接在自定义Generator里使用新版本的analyzerAPI而不需要额外写兼容层。这对于想要把项目的analyzer版本升到最新的人来说是个不小的解脱。4.3 与 Flutter 3.x 和 Dart 3.x 的兼容性现在大部分 Flutter 项目已经跑在 Dart 3.x 上了空安全早已是默认选项。build_runner 2.13 是专门针对 Dart 3 的“新时代”版本它放弃了对某些老旧的 Dart 2 API 的兼容从而在内部实现上可以做得更干净。这一点直接带来的好处是对records和patterns这种 Dart 3 新特性的解析支持更好了。以前你在 model 文件里写一个(String, int)这样的 record 类型freezed 或 json_serializable 可能会因为解析器版本不够而报错。现在用 build_runner 2.13 配合最新版的生成器这类新语法基本都能正常处理。你可以在升级后写一个简单的测试文件验证兼容性import package:freezed_annotation/freezed_annotation.dart; part record_demo.freezed.dart; freezed class RecordDemo with _$RecordDemo { const factory RecordDemo({ required ({String name, int age}) recordField, }) _RecordDemo; }再跑dart run build_runner build如果能够顺利生成record_demo.freezed.dart说明你的生成器工具链已经能很好地支持 Dart 3 语法了。还需要提醒的是build_runner 2.13 对内存占用做了一些优化。以前它在处理大型资源图时容易把内存吃到 1GB 以上新版本通过更及时的垃圾回收和更少的对象复制把峰值内存压低了 20% 到 30% 左右。当然如果你的项目特别大还是建议在 CI 上给 build_runner 单独配置一个 4GB 以上的环境变量environment: sdk: 3.3.0 4.0.05. 实战中常见的坑与排查技巧这一部分算是我从多次升级踩坑里提炼出来的速查表。如果你已经升级到了 build_runner 2.13或者在升级过程中遇到了问题下面这些内容应该能派上用场。5.1 升级后遇到“找不到生成器”怎么办这个故障的典型表现是flutter pub get正常但一跑 build_runner 就报Could not generate because no builder was found。一般来说原因是某个生成器包没有正确声明自己的 builder或者pubspec.yaml里的依赖被覆盖为了一个不兼容的版本。排查步骤很简单检查pubspec.lock里build_runner的传递依赖确认source_gen和analyzer版本是否被锁定在了合理范围内。手动添加你需要的生成器依赖比如 freezed、json_serializable并确认它们是 dev_dependencies 而不是 dependencies。代码生成器放进 dependencies 会让生产包体积变大没必要。执行dart run build_runner build -v在 verbose 日志里搜索builder关键词看看框架到底识别到了哪些 builder。如果识别不到八成是包的入口builders.dart没被正确导出。5.2 增量构建不触发的问题有时候你改完代码跑dart run build_runner build --build-filterlib/**结果发现某些文件没有重新生成。这在 2.13 里虽然比老版本少见了但仍然可能发生。这里有一个非常隐蔽的原因你的文件路径可能被.gitignore忽略了。build_runner 在扫描资源时默认会忽略所有被版本控制忽略的文件哪怕是 lib 目录下的源码。如果你不小心在.gitignore里写了*.freezed.dart或*.g.dart而编译器或 IDE 的自动格式化工具误把这些文件当作项目文件来处理build_runner 的状态就会混乱。解决办法是在 build.yaml 里显式声明不受忽略影响targets: $default: sources: - lib/** - test/** - pubspec.yaml同时千万不要在.gitignore中忽略生成的.g.dart文件。有些团队觉得这些文件可以随时重新生成没必要提交到仓库但对于大型 Flutter 项目强烈建议把它们提交到版本控制里。这样不仅能让增量构建更稳定还能避免 CI 与本地生成的产物不一致导致的诡异问题。5.3 part 文件与手动修改的冲突part文件是整个代码生成体系里最容易被吐槽的设计因为一旦生成的.g.dart里引入了某个不存在的类型主文件编译就会直接报错而且报错信息往往指向的是生成的代码而不是你的源码。我在 2.13 上遇到的一个真实问题是某次我手动改动了一个 model 的字段类型从int?改成了String?然后忘记重新跑 build_runner直接运行flutter analyze。结果报错说type Null is not a subtype of type String in type cast指向的是.g.dart里的fromJson方法。这个问题的本质是生成代码与实际源码不同步。排查方式也简单先猛敲一遍dart run build_runner build --delete-conflicting-outputs然后再看还有没有报错。如果仍然报错就要检查是不是你手动改过.g.dart文件。记住了永远不要手动修改任何.g.dart、.freezed.dart文件这些只是生成产物你要改就改源文件然后重新生成。5.4 内存占用偏高的处理经验即便 2.13 改进了内存遇到特别大的项目它还是会吃掉不少内存。我见过一个极端案例某个项目因为一个循环引用A model 引用 B modelB model 又引用 A model导致代码生成时资源图遍历异常膨胀内存飙升到 2GB 以上最后 OOM 崩溃。遇到这种情况可以从两个方向处理检查是否存在循环的part of引用。两个 dart 文件互为 part 是不合法的会让构建系统陷入死循环式的重新生成。在 build.yaml 中限制某个 builder 的作用范围不要让它在整个 lib 目录下都生效。比如如果你的自定义生成器只需要处理特定子目录就用generate_for把范围收窄builders: my_builder: generate_for: - lib/features/xxx/models/** - lib/core/utils/**这样做不仅能降低内存还能加快构建速度因为生成器不需要扫描所有无关文件了。6. 聊聊我给新项目的默认生成器选型既然 build_runner 已经今非昔比那新项目里到底应该选哪些生成器搭配它这里分享一套我目前比较喜欢的组合供参考。数据模型建模用freezedcustom_lint。freezed 解决不可变数据类和 union 类型的问题它会自己调用 json_serializable所以不需要单独写一堆JsonSerializable()和JsonKey()。状态管理用riverpod_generator它自动把 provider 声明从零散的全局变量收敛成类方法代码可读性高了一个档次。网络层用retrofit_generator配合dio接口定义和实现分离得很干净。唯一要注意的是 retrofit 生成的代码在 2.13 上有一些针对http请求方法的注释解析优化老版本里某些中文注释可能导致生成失败新版本基本没这个问题了。这套组合跑在 build_runner 2.13 上我的体验是一个中等规模的新项目从零开始全量生成大概需要 15 到 20 秒增量构建 2 到 4 秒日常开发完全无感。对比两年前一个类似规模项目动辄一分钟的全量生成这个进步真的很明显。最后再分享一个小技巧升级到 build_runner 2.13 之后我建议你在项目的analysis_options.yaml里加上这个忽略规则不然每次 build 完IDE 的 analyze 窗口都会被一堆 generated_file 相关的 no-op 告警刷屏analyzer: exclude: - **/*.g.dart - **/*.freezed.dart别小看这个配置它能帮你把注意力集中在真正需要关注的代码问题上而不是天天看那些“生成文件不该被手动编辑”的噪音。我个人在实际项目里体会最深的一点是build_runner 2.13 不再像以前那样“跑一次就可能坏一次”它更像一个稳定、可预期的构建基础设施了。如果你之前因为老版本的速度和稳定性问题一直对代码生成抱有偏见那这次升级确实值得再给一次机会。毕竟工具的价值不是看它能不能生成代码而是看它能不能在你项目规模长大之后依然让你感觉不到它的存在。