Dart 62 🎯 Reflection vs Code Generation: dart:mirrors Limitations and Modern AOT Alternatives
Reflection is the ability of a program to inspect and modify its own structure at runtime. In Dart, the dart:mirrors library provides this capability: it can enumerate a class’s methods, read a field’s value by name, construct an instance from a string, and discover the metadata attached to a declaration. The capability is powerful, and it is also incompatible with the ahead-of-time compilation that Flutter and the modern Dart server deployments rely on. The tree shaking that removes the unused code cannot preserve the names the reflection would need, because it cannot know which strings the program will use. The result is the fundamental tension: the reflection enables the dynamic patterns, and the AOT disables the reflection. The code generation is the modern alternative, and the chapter explains why.
Key point: The dart:mirrors library is not supported in the AOT-compiled code, which includes the Flutter release builds and the dart compile exe output. The library works in the JIT mode (the dart run and the dart test), and it is unavailable in the compiled. The code generation is the mechanism that replaces the reflection in the AOT.
Why the reflection and the AOT conflict
The dynamic lookup problem. The reflection resolves a name at the runtime. The reflectClass(SomeClass) and the invoke('methodName') take the strings the program computes. The AOT compiler cannot see the strings, so it cannot know which classes and the members to preserve. The tree shaking removes the members that appear unused, and the reflection fails at the runtime with the NoSuchMethodError.
The tree-shaking problem. The AOT compiler removes the code that is not reachable from the entry point. The reflection is the dynamic reachability: it can access any member that the string names. The compiler treats the reflection as the “everything is reachable” and the tree shaking as the disabled. The trade-off is the size.
The metadata problem. The reflection reads the annotations at the runtime. The AOT compiler strips the metadata because it is the unused in the static. The reflectClass(X).metadata returns the empty in the AOT.
The isolate problem. The dart:mirrors is the per-isolate, and the reflection across the isolates is the not supported. The isolates cannot share the mirror system.
The performance problem. The reflection is the slow. The dynamic lookup, the string comparison, and the boxing are the costs. The static call is the fast, and the reflection is the 10x to the 100x the slower.
The security problem. The reflection can bypass the encapsulation. The private members are the accessible, and the sandbox is the harder. The AOT removes the capability, and the attack surface is the smaller.
The framework problem. The Flutter, the AngularDart (the historical), and the server frameworks used the reflection for the dependency injection, the serialization, and the routing. The migration to the code generation is the framework’s evolution, and the json_serializable, the built_value, and the injectable are the results.
a. The dart:mirrors capabilities
The library provides the introspection and the invocation. The capabilities are the real, and the examples are the illustrative.
The introspection
import 'dart:mirrors';
class Person {
final String name;
final int age;
Person(this.name, this.age);
String greet() => 'Hello, $name';
}
void main() {
final mirror = reflectClass(Person);
print(mirror.declarations.keys);
// The output: the [Symbol('name'), Symbol('age'), Symbol('greet')]
}
The reflectClass returns the ClassMirror, and the declarations enumerates the members.
The dynamic invocation
final instance = Person('Alice', 30);
final mirror = reflect(instance);
final result = mirror.invoke(Symbol('greet'), []).reflectee;
print(result); // The output: the 'Hello, Alice'
The invoke calls the method by the symbol, and the reflectee returns the value.
The metadata
class Route {
final String path;
const Route(this.path);
}
@Route('/home')
class HomePage {}
void main() {
final mirror = reflectClass(HomePage);
final metadata = mirror.metadata;
for (final m in metadata) {
print(m.reflectee); // The output: the Instance of 'Route'
}
}
The metadata returns the annotations, and the reflectee returns the value.
The construction
final classMirror = reflectClass(Person);
final instance = classMirror.newInstance(Symbol(''), ['Alice', 30]).reflectee;
print(instance); // The output: the Instance of 'Person'
The newInstance creates the instance from the constructor symbol and the arguments.
The capabilities table
| The capability | The method |
|---|---|
| The introspection | reflectClass, reflect |
| The invocation | invoke |
| The metadata | metadata |
| The construction | newInstance |
| The field access | getField, setField |
| The type check | isSubtypeOf, isAssignableTo |
b. The AOT limitations
The dart:mirrors is the JIT-only. The AOT compilation fails, or the runtime fails, and the reasons are the structural.
The compile-time error
dart compile exe main.dart -o main
# The error: the dart:mirrors is not supported in the AOT.
The compiler rejects the import in the AOT mode.
The tree shaking
The AOT removes the unused. The reflection needs the unused. The two are the incompatible.
// The reflection uses the string
final name = 'greet';
final method = mirror.invoke(Symbol(name), []);
// The AOT cannot see the string.
// The tree shaking may remove the `greet`.
// The invoke fails with the NoSuchMethodError.
The metadata stripping
The AOT strips the metadata. The reflection reads the empty.
@Route('/home')
class HomePage {}
// The AOT strips the @Route.
// The reflectClass(HomePage).metadata returns the [].
The Flutter release build
The flutter build apk --release uses the AOT, and the dart:mirrors is the unavailable. The debug build uses the JIT, and the reflection works. The behavior differs between the debug and the release, and the debugging is the confusing.
The dart compile exe
The server binary compiled with the dart compile exe is the AOT, and the reflection is the unavailable. The dart run uses the JIT, and the reflection works.
The limitations table
| The limitation | The reason |
|---|---|
| The AOT compile error | The library not supported |
| The tree shaking | The dynamic names |
| The metadata stripping | The static removal |
| The Flutter release | The AOT |
The dart compile exe | The AOT |
c. The code generation alternatives
The code generation replaces the reflection. The build step reads the source, the generator produces the code, and the runtime uses the generated.
The json_serializable
The json_serializable generates the fromJson and the toJson from the annotations.
import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';
@JsonSerializable()
class Person {
final String name;
final int age;
Person(this.name, this.age);
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
The build step generates the person.g.dart with the fromJson and the toJson. The runtime calls the generated, and the reflection is the not needed.
The built_value
The built_value generates the immutable classes with the serialization.
abstract class Person implements Built<Person, PersonBuilder> {
String get name;
int get age;
Person._();
factory Person([void Function(PersonBuilder) updates]) = _$Person;
}
The generated code is the AOT-compatible.
The freezed
The freezed generates the immutable classes, the unions, and the copyWith.
@freezed
class Person with _$Person {
const factory Person({required String name, required int age}) = _Person;
}
The freezed uses the build_runner and the generated.
The injectable
The injectable generates the dependency injection from the annotations.
@injectable
class AuthService {
final ApiClient client;
AuthService(this.client);
}
The generated code wires the dependencies, and the reflection is the not needed.
The retrofit
The retrofit generates the HTTP client from the annotations.
@RestApi()
abstract class ApiClient {
factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
@GET('/users/{id}')
Future<User> getUser(@Path('id') int id);
}
The generated code implements the interface, and the reflection is the not needed.
The build_runner
The build_runner is the tool that runs the generators.
dart run build_runner build
dart run build_runner watch
The build runs the once, and the watch re-runs on the change.
The alternatives table
| The package | The purpose |
|---|---|
json_serializable | The JSON |
built_value | The immutable |
freezed | The immutable, the union |
injectable | The DI |
retrofit | The HTTP |
build_runner | The tool |
The code generation pattern
| The step | The action |
|---|---|
| 1 | The developer writes the source with the annotations |
| 2 | The build_runner runs the generator |
| 3 | The generator produces the .g.dart file |
| 4 | The AOT compiles the source and the generated |
| 5 | The runtime calls the generated |
The reflection versus the code generation
| The aspect | The reflection | The code generation |
|---|---|---|
| The AOT | The not supported | The supported |
| The performance | The slow | The fast |
| The build step | The none | The required |
| The tree shaking | The disabled | The enabled |
| The metadata | The runtime | The compile-time |
| The debugging | The runtime | The compile-time |
| The Flutter | The not in the release | The supported |
Complete Example Session
// ============================================
// PART 1: THE REFLECTION
// ============================================
import 'dart:mirrors';
class Person {
final String name;
final int age;
Person(this.name, this.age);
String greet() => 'Hello, $name';
}
void main() {
final instance = Person('Alice', 30);
final mirror = reflect(instance);
final result = mirror.invoke(Symbol('greet'), []).reflectee;
print(result); // The 'Hello, Alice'
}
// ============================================
// PART 2: THE REFLECTION FAILS IN THE AOT
// ============================================
// dart compile exe main.dart -o main
// The error: the dart:mirrors is not supported.
// ============================================
// PART 3: THE JSON_SERIALIZABLE
// ============================================
import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';
@JsonSerializable()
class Person {
final String name;
final int age;
Person(this.name, this.age);
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
// ============================================
// PART 4: THE GENERATED
// ============================================
// The person.g.dart (the generated)
Person _$PersonFromJson(Map<String, dynamic> json) =>
Person(json['name'] as String, json['age'] as int);
Map<String, dynamic> _$PersonToJson(Person instance) => {
'name': instance.name,
'age': instance.age,
};
// ============================================
// PART 5: THE BUILD_RUNNER
// ============================================
// dart run build_runner build
// ============================================
// PART 6: THE FREEZED
// ============================================
@freezed
class Person with _$Person {
const factory Person({required String name, required int age}) = _Person;
}
// ============================================
// PART 7: THE INJECTABLE
// ============================================
@injectable
class AuthService {
final ApiClient client;
AuthService(this.client);
}
// ============================================
// PART 8: THE RETROFIT
// ============================================
@RestApi()
abstract class ApiClient {
factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
@GET('/users/{id}')
Future<User> getUser(@Path('id') int id);
}
// ============================================
// PART 9: THE AOT COMPATIBLE
// ============================================
// dart compile exe main.dart -o main
// The generated code is the AOT-compatible.
// The reflection is the avoided.
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. The dart:mirrors is the JIT-only.
// 2. The AOT is the Flutter release and the exe.
// 3. The tree shaking removes the dynamic.
// 4. The metadata is the stripped.
// 5. The code generation is the alternative.
// 6. The build_runner runs the generators.
// 7. The json_serializable, the freezed, the injectable.
// 8. The generated code is the AOT-compatible.
// 9. The build step is the cost.
// 10. The runtime is the fast.
The ten parts showed the reflection, the AOT failure, the json_serializable, the generated, the build_runner, the freezed, the injectable, the retrofit, the AOT-compatible, and the complete pattern.
Quick Reference
The dart:mirrors Capabilities
| The capability | The method |
|---|---|
| The introspection | reflectClass, reflect |
| The invocation | invoke |
| The metadata | metadata |
| The construction | newInstance |
| The field | getField, setField |
The AOT Limitations
| The limitation | The reason |
|---|---|
| The compile error | The not supported |
| The tree shaking | The dynamic |
| The metadata | The stripped |
| The Flutter release | The AOT |
The dart compile exe | The AOT |
The Code Generation Packages
| The package | The purpose |
|---|---|
json_serializable | The JSON |
built_value | The immutable |
freezed | The immutable, the union |
injectable | The DI |
retrofit | The HTTP |
build_runner | The tool |
The Comparison
| The aspect | The reflection | The code generation |
|---|---|---|
| The AOT | The not supported | The supported |
| The performance | The slow | The fast |
| The build | The none | The required |
| The tree shaking | The disabled | The enabled |
The AOT Modes
| The mode | The reflection |
|---|---|
The dart run | The supported |
The dart test | The supported |
The dart compile exe | The not supported |
The flutter build apk --debug | The supported |
The flutter build apk --release | The not supported |
Best Practices
✅ Do This:
// Use the json_serializable for the JSON.
@JsonSerializable()
class Person { ... } // ✅
// Use the freezed for the immutable.
@freezed
class Person with _$Person { ... } // ✅
// Use the injectable for the DI.
@injectable
class AuthService { ... } // ✅
# Run the build_runner.
dart run build_runner build // ✅
# The AOT compile.
dart compile exe main.dart -o main // ✅
// Use the generated code.
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json); // ✅
❌ Don’t Do This:
// Don't use the dart:mirrors in the AOT.
import 'dart:mirrors'; // ❌ The compile error
// Don't rely on the runtime metadata.
final meta = reflectClass(X).metadata; // ❌ The stripped
// Don't use the dynamic invoke in the release.
mirror.invoke(Symbol('method'), []); // ❌ The tree shaking
# Don't forget the build_runner.
# The .g.dart is not generated. // ❌
// Don't use the reflection for the DI.
final service = reflectClass(Service).newInstance(...); // ❌ The AOT
// Don't use the reflection for the serialization.
final json = reflect(instance).getField(Symbol('name')); // ❌ The AOT
Common Pitfalls
| The pitfall | The reason | The fix |
|---|---|---|
| The AOT compile error | The dart:mirrors import | Use the code generation |
| The runtime NoSuchMethod | The tree shaking | Use the generated |
| The metadata empty | The stripped | Use the compile-time |
| The release fails | The reflection | The build_runner |
| The bundle large | The reflection preserved | The tree shaking |
| The debug works | The JIT | The release differs |
| The build stale | The no watch | The build_runner watch |
Real-World Examples
1. The Reflection
import 'dart:mirrors';
final mirror = reflect(instance);
mirror.invoke(Symbol('greet'), []);
2. The AOT Error
dart compile exe main.dart -o main
# The dart:mirrors is not supported.
3. The json_serializable
@JsonSerializable()
class Person { ... }
4. The freezed
@freezed
class Person with _$Person { ... }
5. The injectable
@injectable
class AuthService { ... }
6. The retrofit
@RestApi()
abstract class ApiClient { ... }
7. The build_runner
dart run build_runner build
8. The generated
factory Person.fromJson(...) => _$PersonFromJson(json);
9. The AOT Compile
dart compile exe main.dart -o main
10. The Complete Pattern
@JsonSerializable()
class Person { ... }
Visual
The Reflection vs the Code Generation
┌────────────────────────────────────────────────────────────┐
│ THE REFLECTION vs THE CODE GENERATION │
│ │
│ THE REFLECTION │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The runtime │ │
│ │ The reflectClass(X).invoke('method') │ │
│ │ The dynamic, the slow │ │
│ │ The JIT only │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE CODE GENERATION │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The compile-time │ │
│ │ The build_runner → the .g.dart │ │
│ │ The static, the fast │ │
│ │ The AOT compatible │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The AOT Tree Shaking
┌────────────────────────────────────────────────────────────┐
│ THE AOT TREE SHAKING │
│ │
│ THE SOURCE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ class Person { │ │
│ │ String greet() => 'Hello'; │ │
│ │ String unused() => 'Never called'; │ │
│ │ } │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE AOT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The `unused` is removed. │ │
│ │ The `greet` is kept. │ │
│ │ The reflection cannot find the `unused`. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Code Generation Flow
┌────────────────────────────────────────────────────────────┐
│ THE CODE GENERATION FLOW │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The source + the annotation │ │
│ │ @JsonSerializable() │ │
│ │ class Person { ... } │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ The build_runner │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The generated │ │
│ │ person.g.dart │ │
│ │ _$PersonFromJson, _$PersonToJson │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ The AOT │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The compiled │ │
│ │ The static, the fast │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Flutter Debug vs Release
┌────────────────────────────────────────────────────────────┐
│ THE FLUTTER DEBUG vs RELEASE │
│ │
│ THE DEBUG │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The JIT │ │
│ │ The dart:mirrors works │ │
│ │ The reflection available │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE RELEASE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The AOT │ │
│ │ The dart:mirrors fails │ │
│ │ The reflection unavailable │ │
│ │ The code generation required │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The behavior differs between the debug and the release. │
└────────────────────────────────────────────────────────────┘
Summary
| The item | The value |
|---|---|
The dart:mirrors | The reflection library |
| The AOT | The ahead-of-time |
| The tree shaking | The static removal |
| The JIT | The just-in-time |
The json_serializable | The JSON generation |
The freezed | The immutable generation |
The injectable | The DI generation |
The retrofit | The HTTP generation |
The build_runner | The tool |
| The code generation | The alternative |
Key takeaways:
- The
dart:mirrorsis the JIT-only. The library works in thedart runand thedart test, and it is unavailable in thedart compile exeand theflutter build --release. The import fails in the AOT. - The tree shaking is the reason. The AOT removes the code that is not statically reachable. The reflection reaches the code dynamically, and the compiler cannot know which members the strings will name.
- The metadata is the stripped. The annotations are the removed in the AOT, and the
reflectClass(X).metadatareturns the empty. The compile-time annotations are the different from the runtime metadata. - The code generation is the modern alternative. The
json_serializable, thefreezed, theinjectable, and theretrofitgenerate the code from the annotations, and the runtime calls the generated. The reflection is the not needed. - The
build_runnerruns the generators. Thebuildruns the once, and thewatchre-runs on the change. The generated file is the.g.dart. - The generated code is the AOT-compatible. The static calls are the fast, and the tree shaking is the enabled. The bundle is the smaller.
- The Flutter differs between the debug and the release. The debug uses the JIT, and the reflection works. The release uses the AOT, and the reflection fails. The developer should use the code generation from the start.
- The reflection is the slow. The dynamic lookup, the string comparison, and the boxing are the costs. The static is the 10x to the 100x the faster.
Remember: The Dart’s reflection and the AOT are the incompatible. The dart:mirrors is the powerful in the JIT, and the unavailable in the AOT. The code generation is the modern alternative, and the build_runner, the json_serializable, the freezed, the injectable, and the retrofit are the tools. The result is the AOT-compatible, the fast, and the tree-shakeable code.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!