Chuyển đến nội dung chính

Code generation in Dart: build_runner without the frustration

You add json_serializable, run dart run build_runner build, and get:

Conflicting outputs were detected and the build will be terminated.

You run it with --delete-conflicting-outputs, it works, and you type that flag forever afterwards without knowing what it deleted. That is the typical relationship developers have with build_runner, and it is worth fixing, because the tool is more predictable than it looks.

The model: one asset in, one asset out

build_runner is not a script runner. It is a build system over assets — files identified as package:name|path. Every builder declares which extensions it consumes and which it produces, and build_runner constructs a graph from that.

json_serializable says: for every .dart, maybe produce a .g.dart. freezed says: for every .dart, maybe produce a .freezed.dart. Because outputs are keyed by path, two builders that claim the same output path conflict — and so does one builder whose previous output is still on disk from a run with different configuration.

That is what --delete-conflicting-outputs does: it removes generated files that the current build wants to write but that it did not itself create in this run’s cache. It is safe for genuinely generated files, and it is why you should never hand-edit a .g.dart.

The cache lives in .dart_tool/build/. When builds behave impossibly, that directory is the thing to delete:

rm -rf .dart_tool/build && dart run build_runner build --delete-conflicting-outputs

Part files, and the error everyone hits first

Most generators emit part files, which means the generated code shares a library with your source:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';   // required, and the name must match the file exactly

@JsonSerializable()
class User {
  const User({required this.id, required this.name});
  final String id;
  final String name;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Target of URI hasn't been generated before the first build is expected — the analyser is reporting a file that does not exist yet. Run the build; it resolves.

Because it’s a part, the generated code can see your private members, and your file can see the generated _$… functions. It also means one generated file per source file, which is why a package with 200 models has 200 .g.dart files.

Making it fast

Build time is the main complaint, and most of it is fixable through configuration. build.yaml at the package root:

targets:
  $default:
    builders:
      json_serializable:
        generate_for:
          - lib/models/**.dart
        options:
          explicit_to_json: true
          field_rename: snake
      freezed:
        generate_for:
          - lib/models/**.dart

generate_for is the highest-leverage setting in the file. By default a builder is offered every Dart file in your package, and it must at minimum parse each one to decide there is nothing to do. Restricting it to the directory that actually contains annotated classes routinely cuts build time by more than half on a large package.

Other practical measures:

  • Use watch during development, not repeated build. It keeps the asset graph warm and rebuilds only what changed.
    dart run build_runner watch --delete-conflicting-outputs
  • Split large packages. build_runner works per package; a monorepo of five packages rebuilds only the one you touched.
  • Audit your generators. Every codegen dependency taxes every build. A generator saving you thirty lines of boilerplate in two files is not paying for itself.

Which generators earn their keep

My honest ranking, from a codebase-maintenance perspective rather than a feature-count one:

GeneratorVerdict
json_serializableWorth it above ~10 models. Hand-written fromJson is where silent field-name typos live.
freezedWorth it if you use sealed unions and copyWith heavily. Records and sealed classes in modern Dart cover part of what it used to be needed for.
retrofit / API clientsWorth it for large, stable APIs. Overhead for five endpoints.
mockito codegenPrefer hand-written fakes for anything with meaningful behaviour; generated mocks are best for wide interfaces you barely use.
Asset/localisation generatorsAlmost always worth it — they turn runtime string typos into compile errors.

The question I ask before adding one: what class of bug does this prevent? “Less typing” is a weak answer; “a typo in a JSON key is now a compile error” is a strong one.

CI and version control

Two defensible policies, and you should pick one deliberately:

Do not commit generated files (my default). .gitignore gets *.g.dart, *.freezed.dart, and CI runs the build before analysis and tests:

- run: dart pub get
- run: dart run build_runner build --delete-conflicting-outputs
- run: dart analyze --fatal-infos
- run: dart test

Pros: no generated-file diffs in review, no chance of stale output being committed. Cons: every clean checkout pays the build cost, and a generator version bump can break CI without any source change.

Commit them. Checkout is instantly buildable and diffs show exactly what a generator version change did. Cons: noisy pull requests, and merge conflicts in files nobody should be editing.

If you publish a package to pub.dev you must commit the generated files, since consumers do not run your builders.

Either way, pin your generator versions. json_serializable: ^6.0.0 will happily pick up a minor release that changes output formatting and produce a thousand-line diff on an unrelated PR.

Debugging a build that does nothing

When a build reports success but your .g.dart is missing or stale, work through this in order:

  1. Is the part directive present and spelled exactly right?
  2. Is the annotation on the class, and imported from the right package?
  3. Does generate_for in build.yaml actually include this file’s path?
  4. Is the builder in dev_dependencies? A generator in dependencies still runs but bloats consumers.
  5. Run with --verbose and look for the builder’s name against your file.
  6. Delete .dart_tool/build and rebuild.

Step 3 catches more cases than you’d expect, because adding a generate_for for speed and then creating models in a new directory is a very easy sequence to walk into.

FAQ

Why is the first build after pub get so slow?

build_runner compiles the build script itself, including every builder. That kernel is then cached — subsequent builds skip it unless dependencies change.

Can I run build_runner from a Flutter project?

Yes: dart run build_runner build. flutter pub run build_runner is the older form and still works, but dart run is the current spelling.

Is part required?

No — some generators emit standalone libraries you import instead. Part files are the common case because they can access private members.

Does codegen affect app size?

The generated code is real code and is tree-shaken like any other. Serialisation code for models you never use gets removed if nothing references it.

Should I write my own builder?

Only for something specific to your codebase that no package covers, and expect source_gen to take a day to learn. It is a reasonable investment for, say, generating a route table from annotations in a large app.


Asset-graph semantics, build.yaml options including generate_for, part-file requirements, and the behaviour of --delete-conflicting-outputs are documented in the build_runner and build_config references linked above. The generator ranking, the commit-or-not trade-off, the debugging order and the “what class of bug does this prevent” test are my own judgement from maintaining Dart codebases with heavy code generation.


Originally published on FlutterCook. Read the latest version there — that copy is the one kept up to date.

Nhận xét

Bài đăng phổ biến từ blog này

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

Announcing Flutter 2

  Phụ lục: Flutter on the web Flutter 2 on desktops, foldables, and embedded devices The growing Flutter ecosystem Dart: The secret sauce behind Flutter Flutter 2: Available now Hôm nay, chúng tôi sẽ công bố Flutter 2: một bản nâng cấp lớn cho Flutter cho phép các nhà phát triển tạo các ứng dụng đẹp, nhanh chóng và di động cho bất kỳ nền tảng nào. Với Flutter 2, bạn có thể sử dụng cùng một cơ sở mã để gửi các ứng dụng gốc cho năm hệ điều hành: IOS, Android, Windows, macOS và Linux; cũng như trải nghiệm web nhắm mục tiêu các trình duyệt như Chrome, Firefox, Safari hoặc Edge. Flutter thậm chí có thể được nhúng vào ô tô, TV và thiết bị gia dụng thông minh, mang đến trải nghiệm di động và lan tỏa nhất cho thế giới điện toán xung quanh. Mục tiêu của chúng tôi là thay đổi cơ bản cách các nhà phát triển nghĩ về việc xây dựng ứng dụng, bắt đầu không phải với nền tảng bạn đang nhắm mục tiêu mà là với trải nghiệm bạn muốn tạo. Flutter cho phép bạn tạo ra những trải nghiệm tuyệt đẹp trong đó ...