← Articles

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

  1. You annotate classes with markers like @JsonSerializable() or @freezed
  2. You run flutter pub run build_runner build
  3. Generators read the annotations and write .g.dart or .freezed.dart files
  4. You import the generated code in your classes via part directives

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.dart in .gitignore
  • CI must run build_runner build before 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.

Code generation in Flutter with build_runner — ANN Tech