Code generation in Flutter with build_runner
By Ann Tech · 19 January 2026
Code generation in Flutter automates writing repetitive, boilerplate-heavy code. build_runner is the engine — packages like json_serializable, freezed, riverpod_generator, and drift use it to generate Dart files from annotations.
How build_runner works
- You annotate classes with markers like
@JsonSerializable()or@freezed - You run
flutter pub run build_runner build - Generators read the annotations and write
.g.dartor.freezed.dartfiles - You import the generated code in your classes via
partdirectives
Setup
dev_dependencies:
build_runner: ^2.4.8
json_serializable: ^6.7.1
freezed: ^2.4.7
riverpod_generator: ^2.3.0
JSON serialization
import 'package:json_annotation/json_annotation.dart';
part 'product.g.dart';
@JsonSerializable()
class Product {
final String id;
final String name;
final double price;
@JsonKey(name: 'image_url') // Map camelCase to snake_case
final String imageUrl;
@JsonKey(defaultValue: false)
final bool isAvailable;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
required this.isAvailable,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
flutter pub run build_runner build --delete-conflicting-outputs
Freezed for immutable models
freezed generates copyWith, ==, hashCode, and union types:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'order.freezed.dart';
part 'order.g.dart'; // Only if using json_serializable
@freezed
class Order with _$Order {
const factory Order({
required String id,
required List<OrderItem> items,
required OrderStatus status,
required double total,
DateTime? completedAt,
}) = _Order;
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
}
// Union types (sealed classes via freezed)
@freezed
class OrderState with _$OrderState {
const factory OrderState.initial() = OrderStateInitial;
const factory OrderState.loading() = OrderStateLoading;
const factory OrderState.loaded(Order order) = OrderStateLoaded;
const factory OrderState.error(String message) = OrderStateError;
}
// Usage:
state.when(
initial: () => const SizedBox.shrink(),
loading: () => const CircularProgressIndicator(),
loaded: (order) => OrderCard(order: order),
error: (msg) => Text(msg),
);
Watch mode
During development, watch mode re-runs generators automatically on file changes:
flutter pub run build_runner watch --delete-conflicting-outputs
Run this in a terminal while developing. Stop it when done.
Common build_runner errors
Conflict error:
Conflicting outputs were found:
Fix: Add --delete-conflicting-outputs to the build command.
Part file missing:
Error: The getter '_$ProductFromJson' isn't defined
Fix: The generated file hasn't been created. Run the build command.
Circular dependency: Two generated files depend on each other. Refactor to break the cycle.
Generating Riverpod providers
With riverpod_generator:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'products_provider.g.dart';
@riverpod
Future<List<Product>> products(ProductsRef ref) async {
return ref.read(productRepositoryProvider).getProducts();
}
@riverpod
class CartNotifier extends _$CartNotifier {
@override
Cart build() => Cart.empty();
void addItem(Product p) => state = state.copyWith(
items: [...state.items, CartItem(product: p)],
);
}
After build_runner build, productsProvider and cartNotifierProvider are available.
Generated files in git
Two schools of thought:
Check in generated files (recommended for most projects):
- CI doesn't need to run
build_runner - Diffs are reviewable in PRs
- No risk of someone running the app without generated files
Gitignore generated files:
*.g.dart,*.freezed.dartin.gitignore- CI must run
build_runner buildbefore testing - Keeps the repo smaller and cleaner
If you check them in, always regenerate before commit:
flutter pub run build_runner build --delete-conflicting-outputs
git add -u # Stage all modified files including .g.dart
Common pitfalls
Running build_runner on each file save manually. Use watch mode during development — it's significantly faster than repeatedly running the full build.
Importing the part file directly. Don't import 'product.g.dart' — use part 'product.g.dart' in the main file. The generated file is part of the same library, not a separate one.
Not regenerating after dependency upgrades. After running flutter pub upgrade, existing generated files may be stale. Always re-run build_runner build after dependency changes.
Sign in to like, dislike, or report.