Introduction
Dart macros unlock compile-time code generation in Flutter and Dart CLI projects by letting you write meta-programs that inspect and emit Dart source before your app ever runs. Instead of relying on runtime reflection or heavyweight build scripts, you can annotate classes or functions with dart macros to generate boilerplate—think toString(), JSON serializers, copyWith methods, even routing tables—safely and efficiently. This advanced tutorial will guide you through setting up, authoring, and applying macros in your codebase, so you can embrace zero-overhead code gen annotations and turbocharge your development workflow.
Getting Started with Dart Macros
Before writing macros, ensure you’re on Dart 3.5 or later and enable the experiment:
Add to your dart SDK constraints in pubspec.yaml:
environment:
sdk: ">=3.5.0 <4.0.0"
In analysis_options.yaml, enable macros:
analyzer:
enable-experiment
Add dependencies:
dependencies:
macro_annotations: ^0.1.0
dev_dependencies:
build_runner: ^2.4.0
macro_builder
Create a library for your macros, typically in tool/macros/. This isolates meta-code from your runtime.
Defining a Simple Macro
A macro is a class annotated with @Macro that implements hooks like buildDefinition or buildTypes. Here’s a minimal example that generates a toString() override for any annotated class:
import 'package:macro_annotations/macro_annotations.dart';
import 'package:macro_builder/macro_builder.dart';
@Macro()
class ToStringMacro implements ClassDefinitionMacro {
const ToStringMacro();
@override
Future<void> buildDefinition(
ClassDeclaration declaration,
ClassDefinitionBuilder builder,
) async {
final className = declaration.identifier.name;
final fields = declaration.members
.whereType<VariableDeclarationField>()
.map((f) => f.identifier.name)
.toList();
final body = fields
.map((name) => '$name: \$$name')
.join(', ');
builder.addMethod('''
@override
String toString() => '$className($body)';
''');
}
}Key points:
ClassDefinitionMacro inspects the AST of the annotated class.
builder.addMethod emits Dart code into the generated part file.
The macro runs at compile time—no reflection, zero runtime cost.
Applying Macros in Your Codebase
With your macro library set up, consume it in your application or package code:
In your target file, import the macro package and enable the macro:
import 'package:macro_annotations/macro_annotations.dart';
import 'tool/macros/to_string_macro.dart';
part 'user.g.dart';
@ToStringMacro()
class User {
final String name;
final int age;
User(this.name, this.age);
}Run the code generation step:
dart run build_runner build --delete-conflicting-outputs
Inspect user.g.dart, which contains the generated toString() override.
Use User('Alice', 30).toString() and see User(name: Alice, age: 30) printed.
You can integrate this seamlessly into CI, enabling compile-time safety. If you rename or remove fields, the macro regeneration will keep your code in sync without manual edits.
Advanced Usage Patterns
Once you’ve mastered basic macros, explore these advanced techniques:
Macro Composition
Combine multiple macro annotations on a single declaration. For instance, use both ToStringMacro and a JsonSerializableMacro to generate toJson()/fromJson() methods alongside toString().
Parameterized Macros
Accept parameters in your macro annotation to customize behavior:
@generateCopyWith(skip: ['id'])
class Order { }In your macro implementation, read annotation arguments via annotation.arguments.
Error Reporting & Linting
Use builder.reportError() within the macro to provide compile-time diagnostics when unsupported patterns are detected (e.g., private fields).
Macro Testing
Write unit tests for your macros by feeding synthetic code into the macro host and asserting on the generated output string. This ensures stability when Dart updates its AST model.
Cross-Package Sharing
Publish your macros as a Dart package, versioned separately. Consumers simply add your macro package to their dev dependencies and annotate away. This fosters a plugin ecosystem akin to annotation processors in Java/Kotlin.
Performance Considerations
Since macros run in a separate isolate, keep them efficient. Cache schema information and minimize parsing overhead. For large codebases, prefer incremental builds (watch mode) to avoid full rebuilds.
By leveraging these patterns, you can build a powerful suite of code generation macros—think GraphQL client stubs, Flutter widget scaffolds, or state-management bindings—all at compile time with zero runtime penalty.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Dart macros provide a robust, future-proof way to automate repetitive code via compile-time code generation. You gain type safety, maintainability, and performance without the drawbacks of reflection or manual build scripts. By defining custom annotations, hooking into the AST with macro APIs, and employing advanced patterns like composition and error reporting, you can craft highly tailored code generators for your team’s needs. Start small with a toString() macro and progressively introduce serializers, copyWith generators, or routing table builders. Embrace dart macros today to elevate your Flutter and Dart projects with clean, boilerplate-free code.