build_runner представляет собой инструмент для разработки, который прогоняет генераторы кода (builders) по пакету и записывает обычные Dart-файлы вроде user.g.dart. Flutter опирается на него, потому что в нём нет dart:mirrors: рефлексия сломала бы tree shaking и AOT-компиляцию.
Работу делают билдеры: json_serializable, freezed, riverpod_generator и mockito являются билдерами; build_runner находит их в зависимостях, запускает и отслеживает, какие входные файлы дали какие выходные
Аннотации говорят, что генерировать: @JsonSerializable() или @freezed; пакет с аннотациями подключают как обычную зависимость, а build_runner и генераторы лежат в dev_dependencies и в приложение не попадают
На выходе обычный код: его можно прочитать, пройти отладчиком, и в рантайме он ничего лишнего не стоит
Цена: дополнительный шаг сборки, сгенерированные файлы, которые нужно держать актуальными, и ошибки анализатора до первой генерации
FlutterMiddle
Кодогенерация и build_runner
build_runner, part-файлы, json_serializable, freezed, build.yaml, source_gen, время сборки
Закрыто вопросов
0 / 18
- 0Повторить
Вопросы
Сгенерированные функции приватные (начинаются с _), поэтому должны лежать в той же библиотеке, что и ваш класс. Файл через part разделяет область видимости и импорты библиотеки; файл, подключённый через import, был бы отдельной библиотекой и не увидел бы их.
Связь объявляют обе стороны: вы пишете part 'user.g.dart';, генератор пишет part of 'user.dart';. Без вашей строки build_runner предупреждает, что user.g.dart должен быть подключён директивой part, и ничего не записывает
У part-файла нет своих импортов: каждый тип, который использует сгенерированный код, например Address, должен быть импортирован в user.dart
Зачем обёртки: генератор создаёт только функции верхнего уровня; factory User.fromJson и toJson() делают их публичным API класса
Красный код до первой генерации нормален: Target of URI hasn't been generated означает лишь, что user.g.dart ещё не существует
build генерирует один раз и завершается, watch продолжает работать и при каждом сохранении перегенерирует только изменённое, а clean удаляет кэш сборки в .dart_tool/build. --delete-conflicting-outputs (-d) с build_runner 2.7 ничего не делает: устаревшие файлы всегда перезаписываются.
**На что отвечал -d**: старые версии останавливались с «Found N declared outputs which already exist on disk» после свежего клона, смены ветки или flutter clean; -d означал «перезаписать»
Передавать его безвредно: флаг принимается и игнорируется, так что старые скрипты и README продолжают работать
**watch для ежедневной работы: первая сборка платит полную цену, последующие занимают секунды, потому что состояние сборки остаётся в памяти
clean остаётся крайней мерой: он выбрасывает ровно тот кэш, который делает следующую сборку быстрой; используйте его, только когда состояние сборки действительно сломано
Другой конфликт**: «Both X and Y may output Z» значит, что два билдера претендуют на один файл; это исправляется в build.yaml, а не флагом
Аннотациями: fieldRename в @JsonSerializable переименовывает все ключи, @JsonKey(name: ...) переопределяет одно поле, includeFromJson/includeToJson: false исключает поле, а @JsonValue вместе с unknownEnumValue безопасно маппят значения enum.
**fieldRename: FieldRename.snake**: userId читается и пишется как user_id; field_rename: snake в build.yaml применяет это ко всему пакету
**includeFromJson: false, includeToJson: false**: актуальная замена старого ignore: true; полю нужно значение по умолчанию, потому что из JSON оно никогда не заполнится
**@JsonValue('in_progress'): значение элемента enum в JSON; без него используется имя элемента
unknownEnumValue**: без него статус, который сервер добавит через месяц, бросит ArgumentError при декодировании, и упадёт весь ответ
Для приложения обычно нет: добавьте их в gitignore и генерируйте после каждого клона и в CI. Для публикуемого пакета да: dart pub publish пропускает файлы из gitignore, а пользователи пакета ваши билдеры никогда не запускают.
Любая политика работает, только если вся команда соблюдает её одинаково:
В gitignore: свежий клон показывает сотни ошибок анализатора (Target of URI hasn't been generated, неопределённый _$UserFromJson), пока кто-нибудь не запустит build_runner; сделайте генерацию первым шагом скрипта настройки и каждого CI-джоба
В репозитории: клон сразу зелёный, а на ревью видно, что поменялось в сгенерированном коде, но PR становятся шумными; пометьте файлы linguist-generated=true в .gitattributes, чтобы GitHub их сворачивал
Если закоммитили, CI проверяет актуальность: перегенерировать и падать, если рабочее дерево изменилось, иначе в main попадёт устаревший .g.dart
Конфликты слияния в сгенерированных файлах: никогда не разрешайте их руками; возьмите любую сторону и перегенерируйте
Настоящая ошибка в том, чтобы смешать оба подхода: часть файлов в git, часть в gitignore, и на каждой машине своё дерево
Выведется {id: 42, address: Instance of 'Address'}: по умолчанию сгенерированный toJson кладёт в map сам вложенный объект. jsonEncode всё равно даёт правильный JSON лишь потому, что вызывает toJson() у любого объекта, который не умеет кодировать.
Исправление: @JsonSerializable(explicitToJson: true) или explicit_to_json: true в build.yaml для всего пакета; тогда генератор пишет instance.address.toJson() и преобразует списки поэлементно
Где кусается значение по умолчанию: везде, где map используется напрямую: тест, сравнивающий toJson() с map-фикстурой, строка в sqflite, map, переданный в platform channel или записанный в Hive/Firestore, строка лога
Почему по умолчанию выключено: ради обратной совместимости; большинство команд один раз включают опцию в build.yaml и забывают о ней
Первый сработает, два других бросят TypeError. Сгенерированный код только приводит типы: (json['age'] as num).toInt() принимает 30.0, но json['name'] as String отвергает null, а строка '30' не является num.
Никакого приведения: генератор не парсит строки, а отсутствующий ключ и явный null выглядят одинаково: json['name'] в обоих случаях null
Nullable-поля падают молча: bio читается через as String?, поэтому и отсутствие, и null превращаются в null, и различить их уже нельзя
Безымянные ошибки: голый TypeError пишет type 'Null' is not a subtype of type 'String' и не называет ключ
**checked: true**: оборачивает каждое поле, и ошибка приходит как CheckedFromJsonException с className и key; ловите её на границе репозитория
Значения по умолчанию и приведение: @JsonKey(defaultValue: ...) или значение по умолчанию в конструкторе покрывают и отсутствие, и null; разбор '30' делают через @JsonKey(fromJson: ...)
Напишите JsonConverter<T, Json> с fromJson и toJson и повесьте его на поле, на класс или передайте через converters: в @JsonSerializable. Тогда генератор вызывает ваш конвертер вместо приведения типа.
**const-конструктор: конвертер используется как аннотация, поэтому должен быть константным экземпляром
Сопоставление по Dart-типу**: converters: [MoneyConverter()] применяется к каждому полю Money и к элементам List<Money>
Nullable-поля тоже покрыты: JsonConverter<DateTime, int> обрабатывает и DateTime?; сгенерированный код пропускает null мимо конвертера
Разовые случаи: @JsonKey(fromJson: _parse, toJson: _format) с функциями верхнего уровня или статическими делает то же без отдельного класса
**DateTime и так работает**: как строка ISO-8601; конвертер нужен, только если API использует другой формат
Сгенерированный toJson пишет все поля, включая null, поэтому тело запроса выглядит как {"name":"Ann","bio":null,"avatar_url":null}. В PATCH явный null означает «очистить поле», поэтому сервер стирает био и аватар.
Почему: includeIfNull по умолчанию true: генератор создаёт литерал карты со всеми ключами, а nullable-поле, которое никто не задавал, получает просто null, то же самое, что поле, очищенное намеренно
Исправление: includeIfNull: false на классе, @JsonKey(includeIfNull: false) на одном поле или include_if_null: false в build.yaml; тогда генератор создаёт null-aware элементы ('bio': ?instance.bio), и поле со значением null не даёт ключа
Цена: этот DTO больше не может очистить поле; если API очищает через null, отправляйте этот ключ из карты, собранной вручную, или моделируйте «не задано» и «очистить» как разные значения
Как поймать: юнит-тест, что ProfilePatch(name: 'Ann').toJson() равен {'name': 'Ann'}, и интерсептор, который логирует тела запросов в debug-сборках
freezed генерирует == и hashCode по всем полям, toString, copyWith, умеющий и обнулить поле, и классы за factory-конструкторами. Обычного sealed-класса или @JsonSerializable достаточно, когда не нужны ни равенство по значению, ни copyWith.
Два part-файла: user.freezed.dart создаёт freezed, а user.g.dart создаёт json_serializable; freezed лишь подключает к нему fromJson
**Тонкость copyWith**: сгенерированный хелпер отличает «не передано» от «передан null», поэтому copyWith(avatarUrl: null) действительно очищает поле; рукописный copyWith на ?? так не умеет
Union и JSON: при наличии fromJson union получает поле-дискриминатор типа, его пишет json_serializable
**Хватит обычного sealed**: для union, который не покидает приложение и не нуждается ни в copyWith, ни в равенстве по значению: Dart 3 и так даёт исчерпывающий switch и деструктуризацию
**Хватит обычного @JsonSerializable: для DTO, который декодируется один раз и никогда не сравнивается и не копируется: один билдер вместо двух и гораздо меньше сгенерированного кода
Цена**: сотни сгенерированных строк на модель и лишнее время сборки, поэтому тратьте их там, где окупаются равенство и copyWith: в классах состояния и доменных моделях
Freezed 3 требует, чтобы аннотированный класс был abstract (одиночный data-класс) или sealed (union). Point объявлен конкретным классом с миксином _$Point, а в freezed 3 этот миксин объявляет x и y абстрактными геттерами, поэтому анализатор сообщает о нереализованных членах.
Исправление: abstract class Point with _$Point; если factory-конструкторов несколько, используйте sealed class и получите исчерпывающий switch бесплатно
Почему раньше компилировалось: freezed 2 давал каждому геттеру миксина тело-заглушку, бросающее _privateConstructorUsedError, и конкретный класс проходил проверку
Почему поменяли: во freezed 3 появился «mixed mode», где класс с @freezed может быть и обычным классом со своим конструктором и final-полями, поэтому сгенерированный миксин больше не может делать вид, что реализует их
Другой слом freezed 3: when/map удалили в 3.0 и вернули в 3.1 лишь как нерекомендуемые хелперы; задуманной заменой служат switch-выражения
Порядок миграции: обновите freezed и freezed_annotation вместе, перегенерируйте, затем исправляйте ошибки анализатора класс за классом
Freezed использует имя конструктора как значение union, поэтому сгенерированный fromJson ждёт 'screenView', а не 'screen_view'. Каждое такое событие бросает CheckedFromJsonException с Invalid union type "screen_view"!, и ответ, в котором оно есть, не парсится.
Почему тесты проходят: сгенерированный toJson тоже пишет type: screenView, поэтому Event.fromJson(e.toJson()) согласуется сам с собой; расхождение видно только на snake_case-значениях бэкенда
Исправление: unionValueCase: FreezedUnionCase.snake преобразует все имена конструкторов, @FreezedUnionValue('screen_view') переименовывает одно, а union_value_case: snake в build.yaml задаёт это для всего пакета
**fieldRename здесь не поможет**: переименование ключей в json_serializable не трогает значения union; дискриминатор настраивается только в @Freezed
Неизвестные типы: fallbackUnion с конструктором Event.unknown() не даст типу события, который сервер добавит позже, сломать весь ответ
Как поймать: декодируйте фикстуры, скопированные из реальных ответов API, а не объекты, созданные вашим же toJson
build.yaml в корне пакета настраивает билдеры этого пакета: какие файлы читает каждый из них (generate_for), его опции и запускается ли он вообще (enabled). Сузить generate_for до папок с аннотированными классами и есть самый дешёвый способ ускорить сборку.
Ключи билдеров: package:builder, например json_serializable:json_serializable (или просто json_serializable, если имена совпадают) или freezed:freezed; точные имена указаны в собственном build.yaml этого пакета
Опции вместо повторяющихся аннотаций: explicit_to_json, field_rename или include_if_null, заданные здесь, действуют на каждый @JsonSerializable в пакете
Почему сужение помогает: по умолчанию каждый билдер видит каждый Dart-файл пакета; файл, которого билдер не видит, он и не просит анализатор разрешать
Ловушка: generate_for фильтрует входы молча: модель, добавленная вне glob, просто перестаёт генерироваться, и вы узнаёте об этом по неопределённой функции _$...
Ловушка: glob-шаблоны считаются от корня пакета, и для рекурсии нужен **: lib/models/*.dart пропускает подпапки
ARB-файлы не входят в пайплайн build_runner. AppLocalizations генерирует flutter gen-l10n, команда Flutter tool, которую настраивает l10n.yaml; она же запускается при flutter run и flutter build, если в pubspec.yaml задано generate: true.
Результат ложится в исходники: файлы пишутся в arb-dir по умолчанию (output-dir, например lib/l10n, переносит их) и импортируются как обычно; синтетический пакет package:flutter_gen/gen_l10n/... удалён из Flutter tool, а ключ synthetic-package больше ничего не делает
С типизированными ассетами история отдельная: Assets.images.logo создаёт flutter_gen_runner, а вот он как раз билдер build_runner
Пропущенные переводы молчат: ключ, которого нет в app_ru.arb, откатывается к языку шаблона; задайте untranslated-messages-file (когда пропусков нет, туда пишется {}) и роняйте CI, если в файле что-то другое
Кто что генерирует: json_serializable, freezed, riverpod_generator, mockito, drift: build_runner; AppLocalizations: flutter gen-l10n; GeneratedPluginRegistrant: Flutter tool при каждой сборке
В основном на то, что анализатор разрешает библиотеки для билдеров: каждый аннотированный файл разрешается вместе со всем, что он импортирует. Время возвращается, если меньше файлов доходит до меньшего числа билдеров, кэш сборки остаётся тёплым, а build_runner свежим.
Куда уходит время
Разрешение, а не запись: модель, импортирующая файл с виджетами, тащит через анализатор весь слой виджетов и всё, что тот импортирует
Каждый билдер на каждом входе: без generate_for json_serializable, freezed и mockito просматривают каждый Dart-файл пакета
Холодные старты: clean, удалённый .dart_tool/build или свежий CI-раннер заставляют пересобирать всё
Что возвращает время
Сузить входы: glob-шаблоны generate_for в build.yaml, чтобы каждый билдер видел только свои папки; новые билдеры также поддерживают run_only_if_triggered и пропускают файлы без своей аннотации
Держать импорты моделей маленькими: DTO и классы состояния в собственных библиотеках или пакетах разрешаются гораздо быстрее
**Использовать watch: первая сборка холодная, следующие инкрементальные и занимают секунды
Кэшировать .dart_tool/build в CI**: с ключом по pubspec.lock и версии SDK; никогда не запускать clean по привычке
Обновиться: build_runner 2.10–2.14 добавил AOT-компиляцию билдеров (включена по умолчанию) и несколько ускорений инкрементальной сборки в 2–4 раза; старая зафиксированная версия лишает вас этого
Сначала измерить: лог показывает, какой билдер медленный, а --build-filter перегенерирует один выходной файл, когда нужен только он
Унаследуйтесь от GeneratorForAnnotation<T> и верните генерируемый код из generateForAnnotatedElement, оберните генератор в SharedPartBuilder внутри factory-функции верхнего уровня и объявите эту функцию в build.yaml пакета-генератора.
Три пакета: аннотация (чистый Dart, обычная зависимость), генератор (зависит от analyzer, build, source_gen; dev-зависимость приложения) и само приложение
**element**: разрешённая анализатором модель аннотированного объявления: поля, типы, супертипы. Поэтому каждый мажорный релиз analyzer прокатывается по всем генераторам
**ConstantReader annotation**: читает вычисленную аннотацию, например annotation.read('name').stringValue
Ошибки: бросайте InvalidGenerationSourceError(message, element: element), чтобы сборка указала на объявление пользователя, а не выдала сломанный код
**SharedPartBuilder**: пишет фрагмент .analytics.g.part, который source_gen:combining_builder сливает в общий .g.dart; для собственного файла есть PartBuilder или LibraryBuilder
**Без dart:mirrors**: в source_gen 4 TypeChecker.fromRuntime заменили на TypeChecker.typeNamed, чтобы билдеры можно было компилировать AOT; не используйте mirrors и в своём коде
Тестируйте: testBuilder из build_test запускает билдер на исходниках в памяти и сравнивает результат
Билдер применяется только к пакетам, которые напрямую зависят от его пакета (auto_apply: dependents), а обычный запуск пишет выходные файлы только в тот пакет, где его запустили. Каждому feature-пакету нужна своя dev-зависимость на генератор, а генерация идёт по пакетам или с --workspace.
Ребро зависимости: feature_cart должен указать json_serializable (или ваш core_gen) в собственных dev_dependencies; если он есть только в app/, к feature_cart ничего не применяется
Разделяйте аннотации и генераторы: core_annotations является чистым Dart и обычной зависимостью; core_gen тянет analyzer, build и source_gen и остаётся dev-зависимостью, так что ничего из этого не попадает в сборку
Pub workspaces: со списком workspace: в корневом pubspec.yaml (Dart 3.6+) команда dart run build_runner build --workspace (build_runner 2.11+, стабильна с 2.14) собирает все пакеты одним процессом с общим анализатором
Без workspace: melos exec --depends-on=build_runner -- dart run build_runner build запускает генерацию в каждом пакете; добавьте --order-dependents, если генератор одного пакета читает сгенерированный код другого
Единое разрешение зависимостей означает один анализатор: все генераторы в workspace должны принимать одну версию analyzer, поэтому один устаревший генератор может заблокировать обновления для всех пакетов
Команда Dart отменила макросы в январе 2025 года, до их выхода: выполнение пользовательского кода внутри компилятора и анализатора на каждое нажатие клавиши слишком дорого обходилось скорости IDE и hot reload. Кодогенерация остаётся на build_runner, и усилия ушли в его ускорение.
Мигрировать нечего: макросы существовали только за экспериментальным флагом; @JsonCodable так и не попал в стабильный SDK
Что изменилось вместо этого: релизы build_runner 2025–2026 годов принесли AOT-компиляцию билдеров, намного более быструю инкрементальную сборку и сборку с --workspace; генераторы отказались от dart:mirrors, чтобы AOT стал возможен
С augmentations история отдельная: языковое предложение, не часть отмены макросов, без даты выхода; они позволят сгенерированному part-файлу добавлять члены прямо в ваш класс (без with _$User и обёрток к _$UserFromJson) и изменят вид сгенерированного кода, но не способ его получения
Практическая позиция: продолжайте использовать json_serializable, freezed и riverpod_generator, а время сборки держите под контролем через generate_for, watch и кэш .dart_tool/build