| |

Dart 62 🎯 Reflection vs Code Generation: dart:mirrors Limitations and Modern AOT Alternatives

⏱️ Estimated read: 14 min | |

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 capabilityThe method
The introspectionreflectClass, reflect
The invocationinvoke
The metadatametadata
The constructionnewInstance
The field accessgetField, setField
The type checkisSubtypeOf, 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 limitationThe reason
The AOT compile errorThe library not supported
The tree shakingThe dynamic names
The metadata strippingThe static removal
The Flutter releaseThe AOT
The dart compile exeThe 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 packageThe purpose
json_serializableThe JSON
built_valueThe immutable
freezedThe immutable, the union
injectableThe DI
retrofitThe HTTP
build_runnerThe tool

The code generation pattern

The stepThe action
1The developer writes the source with the annotations
2The build_runner runs the generator
3The generator produces the .g.dart file
4The AOT compiles the source and the generated
5The runtime calls the generated

The reflection versus the code generation

The aspectThe reflectionThe code generation
The AOTThe not supportedThe supported
The performanceThe slowThe fast
The build stepThe noneThe required
The tree shakingThe disabledThe enabled
The metadataThe runtimeThe compile-time
The debuggingThe runtimeThe compile-time
The FlutterThe not in the releaseThe 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 capabilityThe method
The introspectionreflectClass, reflect
The invocationinvoke
The metadatametadata
The constructionnewInstance
The fieldgetField, setField

The AOT Limitations

The limitationThe reason
The compile errorThe not supported
The tree shakingThe dynamic
The metadataThe stripped
The Flutter releaseThe AOT
The dart compile exeThe AOT

The Code Generation Packages

The packageThe purpose
json_serializableThe JSON
built_valueThe immutable
freezedThe immutable, the union
injectableThe DI
retrofitThe HTTP
build_runnerThe tool

The Comparison

The aspectThe reflectionThe code generation
The AOTThe not supportedThe supported
The performanceThe slowThe fast
The buildThe noneThe required
The tree shakingThe disabledThe enabled

The AOT Modes

The modeThe reflection
The dart runThe supported
The dart testThe supported
The dart compile exeThe not supported
The flutter build apk --debugThe supported
The flutter build apk --releaseThe 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 pitfallThe reasonThe fix
The AOT compile errorThe dart:mirrors importUse the code generation
The runtime NoSuchMethodThe tree shakingUse the generated
The metadata emptyThe strippedUse the compile-time
The release failsThe reflectionThe build_runner
The bundle largeThe reflection preservedThe tree shaking
The debug worksThe JITThe release differs
The build staleThe no watchThe 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 itemThe value
The dart:mirrorsThe reflection library
The AOTThe ahead-of-time
The tree shakingThe static removal
The JITThe just-in-time
The json_serializableThe JSON generation
The freezedThe immutable generation
The injectableThe DI generation
The retrofitThe HTTP generation
The build_runnerThe tool
The code generationThe alternative

Key takeaways:

  • The dart:mirrors is the JIT-only. The library works in the dart run and the dart test, and it is unavailable in the dart compile exe and the flutter 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).metadata returns the empty. The compile-time annotations are the different from the runtime metadata.
  • The code generation is the modern alternative. The json_serializable, the freezed, the injectable, and the retrofit generate the code from the annotations, and the runtime calls the generated. The reflection is the not needed.
  • The build_runner runs the generators. The build runs the once, and the watch re-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.


Was this helpful?


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!