← Back to blog
Dart Internals & Performance · Part 8 of 8
September 12, 202632 min read

100 Questions to Master Dart Internals & Performance

DartPerformanceFlutter

100 Questions to Master Dart Internals & Performance

This is Part 8 — the finale of the Dart Internals & Performance series. The previous seven parts taught the machinery; this is where you prove you own it.

How to use this bank:

  • 100 questions, grouped by topic, tagged [Basic], [Medium], [Advanced] and marked (Theory) or (Coding).
  • Each has a Hint (the idea / where to look) and a separate Solution (the actual answer). Try cold first, peek at the hint if stuck, only then check the solution. The struggle is the learning.
  • For (Coding) questions, run your code — dartpad.dev needs zero setup. Predict the output before running.
  • After the 100, there are 10 coding mini-exercises with full solutions, ending in a capstone.

If you can explain the why on all 100 without hints, you genuinely understand what happens beneath your Dart. Let's go.


Section A — The toolchain & runtime (Q1–14)

Q1. [Basic] (Theory) What does the Common Front End (CFE) produce, and why is it shared?

Hint

Think .dill. See Part 1.

Solution

It produces kernel (.dill) — parsed, type-checked, desugared intermediate code. It's shared so every backend (JIT VM, AOT, dart2js, Wasm) reuses one parser/type-checker instead of re-implementing it.

Q2. [Basic] (Theory) Name Dart's "two engines" and what each is used for.

Hint

Development vs production.

Solution

JIT (development — dart run, flutter run, enables hot reload) and AOT (production — flutter build, dart compile exe).

Q3. [Basic] (Theory) What is a snapshot?

Hint

A "save file" for program state.

Solution

A serialized image of program state (code and/or objects) written to a file so it can be loaded quickly later without redoing parse/compile work.

Q4. [Medium] (Theory) What does an AOT snapshot contain that a kernel snapshot doesn't?

Hint

Machine code, plus a heap.

Solution

Native machine code plus a pre-initialized heap of already-built objects (including compile-time constants). A kernel snapshot only holds the kernel AST.

Q5. [Medium] (Theory) Why does even an AOT-compiled program still need a runtime?

Hint

GC, scheduler, type checks...

Solution

The runtime provides garbage collection, the async event-loop scheduler, runtime type checks, isolate management, and core-library natives. AOT compiles your code but doesn't remove the need for these services; the runtime ships embedded.

Q6. [Basic] (Coding) Which command produces a self-contained native executable from bin/main.dart?

Hint

dart compile ...

Solution
dart compile exe bin/main.dart -o app

Q7. [Medium] (Theory) How does the kernel format make hot reload possible?

Hint

Incremental + inject into running VM.

Solution

On reload, the CFE recompiles only changed libraries into a kernel delta and injects it into the running VM, which swaps code while keeping state. The compact, incrementally-producible kernel is what keeps the round trip sub-second.

Q8. [Medium] (Theory) In development, describe what the VM does to a function called a million times in a loop.

Hint

Interpret → profile → optimize → maybe deopt.

Solution

It starts interpreted/baseline so it runs immediately, profiles call counts and types, and once "hot" the optimizing JIT compiles specialized machine code based on observed types — deoptimizing if an assumption is later violated.

Q9. [Advanced] (Theory) Why can the JIT sometimes outperform AOT on steady-state hot code?

Hint

Runtime knowledge AOT lacks.

Solution

The JIT observes real runtime types and frequencies and compiles speculatively specialized code (concrete types, inlining, devirtualization), deoptimizing if wrong. AOT compiles before any execution and must stay conservative. For long-lived hot paths, profile-guided specialization can win.

Q10. [Basic] (Theory) Which Flutter build mode uses JIT?

Hint

The one with hot reload.

Solution

Debug (flutter run). Profile and release are AOT.

Q11. [Medium] (Coding) What does this command produce, and how do you run its output?

dart compile aot-snapshot bin/main.dart -o app.aot
Hint

Needs a runtime to load it.

Solution

An AOT machine-code snapshot (not a standalone exe). Run it with the AOT runtime:

dartaotruntime app.aot

Q12. [Medium] (Theory) For the web, how does Dart execute — JIT or AOT to machine code?

Hint

Neither — different target.

Solution

Neither; for web Dart compiles to JavaScript (dart compile js) or WebAssembly (dart compile wasm), and the browser engine runs it. Same CFE front end, different backend.

Q13. [Advanced] (Theory) Why does a trivial "Hello World" Dart executable weigh a few megabytes?

Hint

What ships inside it?

Solution

It bundles the embedded Dart runtime (GC, scheduler, type system, core-library natives) alongside your compiled code. The runtime is the bulk of a tiny program's size.

Q14. [Advanced] (Theory) Hot reload vs hot restart — what's preserved in each?

Hint

State vs fresh main().

Solution

Hot reload injects new code and preserves app state (no main() re-run). Hot restart discards state and re-runs main() from scratch. Both require the JIT/debug build.


Section B — JIT vs AOT (Q15–29)

Q15. [Basic] (Theory) State the one-line difference between JIT and AOT.

Hint

It's about timing.

Solution

JIT compiles while the program runs; AOT compiles before it runs. Every other trade-off follows from that.

Q16. [Basic] (Theory) What is "warm-up," and which engine has it?

Hint

Slow until hot paths optimize.

Solution

The early period where JIT code runs interpreted/baseline while being profiled before the optimizing compiler kicks in. AOT has no warm-up — it's pre-compiled.

Q17. [Medium] (Theory) What is tree shaking, which engine does it, and why can it?

Hint

Whole-program reachability.

Solution

Dead-code elimination — dropping anything unreachable from main(). AOT does it because it has the whole program statically before running, so it can prove what's unused. JIT can't (code can be added at runtime).

Q18. [Basic] (Coding) Will helperB ship in the release build?

void helperA() => print('A');
void helperB() => print('B');
void main() => helperA();
Hint

Is it reachable from main?

Solution

NohelperB is unreachable, so AOT tree-shakes it out. helperA stays.

Q19. [Medium] (Theory) Why is iOS App Store policy a reason Flutter ships AOT there?

Hint

Runtime code generation.

Solution

A JIT generates machine code at runtime (writable+executable memory), which iOS prohibits. AOT produces fixed native code ahead of time and complies.

Q20. [Medium] (Theory) Name Flutter's three build modes and which compiler each uses.

Hint

debug / profile / release.

Solution

Debug = JIT (hot reload), Profile = AOT (+ DevTools hooks), Release = AOT (optimized, shipped).

Q21. [Medium] (Theory) Why must you never benchmark performance in debug mode?

Hint

Asserts on, optimizations off.

Solution

Debug is JIT with assertions on and optimizations effectively off (plus warm-up), so timings are unrepresentative and often many times slower than release. Profile in --profile/release.

Q22. [Advanced] (Theory) What is deoptimization and when does it happen?

Hint

A broken speculative assumption.

Solution

When optimized JIT code's speculative assumption (e.g. "always int") is violated at runtime, the VM deoptimizes — bails to the safe, general version — and may re-optimize later. It's how the JIT stays correct while being aggressive.

Q23. [Medium] (Coding) A colleague benchmarks a sort in flutter run and says Dart is slow. What's the methodological fix?

Hint

Wrong build mode.

Solution

Re-measure in profile or release (AOT) mode. Debug timings (JIT, asserts on) are not representative.

Q24. [Advanced] (Theory) What is devirtualization and why is AOT good at it?

Hint

Polymorphic → direct call.

Solution

Turning a polymorphic (virtual) call into a direct one when analysis proves a single possible target. AOT's whole-program analysis lets it find such cases (e.g. a class with one implementation), removing dispatch overhead.

Q25. [Basic] (Theory) Which engine enables hot reload, and why only that one?

Hint

Inject code into a running program.

Solution

JIT — only it can inject new code into an already-running program. AOT code is frozen and has no compiler present.

Q26. [Medium] (Theory) Why are AOT binaries typically larger than the code in a JIT setup?

Hint

Every reachable function's machine code.

Solution

AOT ships native machine code for all reachable functions up front (plus the runtime), whereas JIT ships compact kernel and compiles lazily. (AOT offsets this with tree shaking.)

Q27. [Advanced] (Theory) Give one scenario where AOT's predictability matters more than peak throughput.

Hint

Frame times.

Solution

Real-time UI rendering: AOT has no mid-run deopt/recompile pauses, giving consistent frame times — preferable to a JIT that might pause to recompile during an animation, even if its peak throughput is similar.

Q28. [Medium] (Coding) Which build do you use to measure real frame times with DevTools' timeline?

Hint

AOT + tooling hooks.

Solution

Profile mode: flutter run --profile. AOT timings with enough hooks for the timeline/CPU profiler.

Q29. [Advanced] (Theory) Summarize why Flutter bothers to use both engines instead of one.

Hint

Different priorities at dev vs ship time.

Solution

Development prizes iteration speed (hot reload → JIT); shipping prizes startup, predictability, and platform compliance (→ AOT). Using each where it wins gives sub-second reload and fast, jank-free release apps.


Section C — Garbage collection & memory (Q30–44)

Q30. [Basic] (Theory) What determines whether an object is garbage in Dart?

Hint

Can the roots reach it?

Solution

Reachability from the roots (stack locals, statics, globals). Unreachable objects are garbage. There's no manual free.

Q31. [Basic] (Theory) State the generational hypothesis.

Hint

Most objects...

Solution

Most objects die young — created, used briefly, discarded — while a few live long. Dart's GC is built around this.

Q32. [Medium] (Theory) How does the new-space scavenger work, and why is it cheap?

Hint

Copy survivors only.

Solution

New space has two halves; when one fills, the scavenger copies live objects to the other half and frees the old half wholesale. Cost is proportional to survivors, not garbage, so high churn with few survivors is cheap.

Q33. [Medium] (Theory) Why is allocating in new space nearly free?

Hint

Pointer bump.

Solution

Allocation is a bump-pointer in a contiguous region — advance the "next free" pointer. That's why making lots of short-lived widgets is cheap.

Q34. [Medium] (Theory) What manages old space, and what extra step fights fragmentation?

Hint

Mark, sweep, and...

Solution

A mark-sweep collector, with compaction (sliding live objects together) to keep free memory contiguous.

Q35. [Advanced] (Theory) How does Dart keep old-space GC from causing animation jank?

Hint

Concurrent + idle.

Solution

Concurrent marking (and parallel phases) keeps stop-the-world pauses short, and Flutter signals idle periods so GC runs between frames rather than mid-animation.

Q36. [Basic] (Coding) After this, which User is eligible for collection?

var a = User('1');
var b = User('2');
a = b;
Hint

What does nothing point to?

Solution

User('1') — nothing references it after a = b. User('2') is alive (both a and b point to it).

Q37. [Medium] (Theory) "GC'd languages can't leak." Why is that false in Dart?

Hint

Reachable ≠ needed.

Solution

GC only reclaims unreachable objects. Keeping something reachable you no longer need — undisposed subscriptions/controllers, closures capturing this, unbounded caches — leaks it. The GC works; the references are the bug.

Q38. [Medium] (Coding) Find and fix the leak.

class _S extends State<W> {
  late final StreamSubscription _sub;
  @override
  void initState() {
    super.initState();
    _sub = stream.listen(_onData);
  }
}
Hint

What's missing?

Solution

No dispose cancels _sub, so the subscription keeps the State alive. Fix:

@override
void dispose() {
  _sub.cancel();
  super.dispose();
}

Q39. [Medium] (Theory) What happens to an object that survives several scavenges, and why?

Hint

Promotion.

Solution

It's promoted to old space. Surviving repeated scavenges marks it as long-lived (generational hypothesis), so it stops being repeatedly copied and is handled by the old-space collector.

Q40. [Advanced] (Theory) How do isolates change the GC picture?

Hint

Separate heaps.

Solution

Each isolate has its own heap and GC (no shared mutable memory; messages are copied/transferred), so GC in one never pauses another. Offloading work to a background isolate keeps its allocation churn off the UI isolate.

Q41. [Advanced] (Coding) You cache decoded images by URL but don't want the cache to keep them alive. What tool, and why not a plain Map?

Hint

Weak.

Solution

Use WeakReference<Image> (or a bounded/LRU cache). A plain Map<String, Image> keeps every value strongly reachable, so the GC can never reclaim them — an unbounded cache becomes a leak.

Q42. [Medium] (Theory) What is Finalizer for, and what's the catch?

Hint

Cleanup after collection, non-deterministic.

Solution

It runs a cleanup callback after an object is collected (e.g. releasing a native resource). The catch: timing is non-deterministic and best-effort, so prefer explicit dispose() and use it only as a safety net.

Q43. [Advanced] (Coding) Why is this an "effective leak," and how do you bound it?

class Cache {
  static final _m = <String, Uint8List>{};
  static void put(String k, Uint8List v) => _m[k] = v;
}
Hint

Never evicts.

Solution

_m only grows and is static (always reachable), so all values stay alive forever. Bound it with an LRU/size cap (evict oldest on insert) or use WeakReference values.

Q44. [Advanced] (Theory) Describe the DevTools workflow to confirm a screen leaks on push/pop.

Hint

Snapshots + retaining path.

Solution

Repeat push/pop several times, take a heap snapshot (or diff two), check whether instances of that screen's State accumulate, and follow the retaining path to the reference you failed to release in dispose().


Section D — const & compile-time constants (Q45–58)

Q45. [Basic] (Theory) Core difference between final and const?

Hint

When is the value known?

Solution

final is assigned once at runtime; const is a compile-time constant, known before the program runs and deeply immutable.

Q46. [Basic] (Coding) Which fail to compile?

final a = DateTime.now();
const b = DateTime.now();
const c = [1, 2, 3];
Hint

Is DateTime.now() known at compile time?

Solution

Only b fails — DateTime.now() isn't a constant expression. a and c are fine.

Q47. [Medium] (Theory) Requirements for a class to have a const constructor?

Hint

Immutability.

Solution

All instance fields final, empty constructor body (init via initializer list/field initializers), and no non-const superclass generative constructor. To invoke as const, all arguments must be constant.

Q48. [Medium] (Coding) Predict the output.

const p = Point(1, 1);
const q = Point(1, 1);
print(identical(p, q));
Hint

Canonicalization.

Solution

true — identical const values are canonicalized to one shared instance.

Q49. [Medium] (Theory) What is canonicalization?

Hint

One instance program-wide.

Solution

For a given const expression, Dart creates exactly one shared instance and reuses it everywhere, so equal constants are identical().

Q50. [Advanced] (Theory) Explain the two distinct ways const helps Flutter performance.

Hint

Allocation + rebuilds.

Solution

(1) No allocation — a const widget is a canonical instance built once (into the snapshot heap), never re-allocated. (2) Skipped rebuilds — its identity stays the same, so Flutter can prune that subtree from the rebuild/diff walk.

Q51. [Medium] (Coding) Make this const-able.

class Vector {
  double x, y;
  Vector(this.x, this.y);
}
Hint

Fields + constructor.

Solution
class Vector {
  final double x, y;
  const Vector(this.x, this.y);
}

Q52. [Medium] (Theory) Does a const constructor force compile-time construction? Use Foo(runtimeValue).

Hint

Call-site const.

Solution

No. It enables it. Foo(runtimeValue) builds at runtime because the argument isn't constant. You need const at the call site (or an enclosing const context) and constant arguments.

Q53. [Advanced] (Theory) In a release build, where does const Point(0,0) physically live, and what does that save?

Hint

Snapshot heap.

Solution

In the AOT snapshot's pre-initialized heap (Part 1) — built at compile time. Saves runtime allocation and any GC work for it.

Q54. [Basic] (Coding) Why does the IDE suggest const here?

Icon(Icons.star, color: Colors.amber)
Hint

It never changes.

Solution

It's fully constant, so const Icon(...) is one canonical instance reused every rebuild (no re-allocation, prunable on rebuild). Hence the prefer_const_constructors lint.

Q55. [Medium] (Theory) Why must switch case labels be constant?

Hint

Compiler evaluates them.

Solution

Case labels are resolved by the compiler into constant dispatch and must be known/comparable at compile time, so non-constant expressions aren't allowed.

Q56. [Medium] (Coding) Inside const Padding(padding: EdgeInsets.all(8), child: Text('Hi')), do you need const on EdgeInsets.all(8)?

Hint

Propagation.

Solution

No — inside an already-const context, const propagates downward, so EdgeInsets.all(8) and Text('Hi') are implicitly const.

Q57. [Advanced] (Theory) Why is equality of canonical const objects effectively free?

Hint

Same instance.

Solution

Identical constants are the same object, so identical() (and == short-circuiting on identity) returns instantly without field comparison — which Flutter leverages to detect unchanged widgets.

Q58. [Advanced] (Coding) Does final w = Icon(Icons.star) get the canonical instance? Why or why not?

Hint

Is const present?

Solution

No. Without const at the call site it's a runtime allocation, not the canonicalized compile-time object. You must write const Icon(Icons.star) to get the shared instance.


Section E — Dart FFI (Q59–72)

Q59. [Basic] (Theory) What is FFI and what does dart:ffi enable?

Hint

Foreign functions.

Solution

Foreign Function Interface — calling native (C) code directly. dart:ffi loads native libraries, describes signatures, marshals arguments, and works with native pointers/structs.

Q60. [Medium] (Theory) FFI vs platform channels — key difference?

Hint

Sync call vs async message.

Solution

FFI is a direct synchronous native call in the same process (low overhead, C-level responsibility). Platform channels are asynchronous message passing to Kotlin/Swift (serialized, thread-hopping).

Q61. [Medium] (Coding) Write the two typedefs + lookup for C's double scale(double v, int factor);.

Hint

Native types vs Dart types.

Solution
typedef NativeScale = Double Function(Double, Int32);
typedef DartScale = double Function(double, int);
final scale = lib.lookupFunction<NativeScale, DartScale>('scale');

Q62. [Medium] (Theory) Why two signatures per FFI function?

Hint

ABI vs friendly API.

Solution

The native signature (Int32, Double, Pointer<T>) describes the exact C ABI/bit layout; the Dart signature (int, double) is the friendly API. lookupFunction marshals between them.

Q63. [Basic] (Theory) Who frees malloc/calloc memory, and why?

Hint

Not the GC.

Solution

You do. Native allocations live outside the Dart heap, so the GC doesn't track them. Every malloc/calloc needs a matching free, or you leak.

Q64. [Medium] (Coding) Fix the leak.

void greet(String name) {
  final c = name.toNativeUtf8();
  nativeGreet(c);
}
Hint

toNativeUtf8 allocates native memory.

Solution
void greet(String name) {
  final c = name.toNativeUtf8();
  try {
    nativeGreet(c);
  } finally {
    calloc.free(c);
  }
}

Q65. [Medium] (Theory) What does .ref do on a Pointer<MyStruct>?

Hint

Dereference.

Solution

It dereferences the pointer to the struct instance backed by that native memory, so p.ref.field = x writes directly into the C layout.

Q66. [Medium] (Coding) Declare a Dart struct for C's typedef struct { double x; double y; } Coord;.

Hint

extends Struct, @Double().

Solution
final class Coord extends Struct {
  @Double()
  external double x;
  @Double()
  external double y;
}

Q67. [Advanced] (Theory) Why can one FFI call jank the UI, and the fix?

Hint

Synchronous on the thread.

Solution

FFI calls run synchronously on the calling isolate's thread; a long native call blocks the UI isolate's event loop so frames can't paint. Fix: run it on a background isolate (e.g. Isolate.run).

Q68. [Advanced] (Theory) NativeCallable.isolateLocal vs listener — when is each required?

Hint

Same thread vs other thread.

Solution

isolateLocal is synchronous and must be invoked on the owning isolate's thread (use for same-thread callbacks needing a return). listener posts asynchronously to the isolate's event queue — required for callbacks fired from another thread.

Q69. [Basic] (Theory) What's the native type mapping for C double vs float?

Hint

Both Dart double.

Solution

C double → ffi Double, C float → ffi Float; both surface as Dart double. The native type controls bit width.

Q70. [Medium] (Theory) What does ffigen do and why use it?

Hint

Parse headers.

Solution

It parses C headers and generates type-safe Dart bindings, so you don't hand-write the two-signature dance for large libraries. (It's build-time codegen — Part 7.)

Q71. [Advanced] (Coding) Why is sending a raw Pointer to another isolate problematic?

Hint

Separate heaps; raw addresses.

Solution

Isolates have separate heaps and messages are copied; a Pointer is a raw native address with no managed wrapper to copy safely, and ownership/lifetime get murky. Typically you perform the whole native interaction inside one isolate (passing the address as an int only when you really know what you're doing).

Q72. [Advanced] (Theory) What is NativeFinalizer and how does it relate to GC?

Hint

Free native memory when the Dart wrapper dies.

Solution

It ties a native resource's cleanup (e.g. free) to a Dart object's lifecycle: when the wrapper is collected, the finalizer runs. It's a safety net for native memory the GC can't manage — but explicit dispose() is preferred since finalizer timing is non-deterministic.


Section F — Writing efficient Dart (Q73–86)

Q73. [Basic] (Theory) Why profile in profile/release, not debug?

Hint

JIT + asserts.

Solution

Debug is JIT with asserts on and optimizations off — unrepresentative. Profile mode is AOT (real timings) with DevTools hooks.

Q74. [Basic] (Theory) In a flame chart, what does box width mean and which do you fix?

Hint

Width = time.

Solution

Width = time spent in that function (+ callees). Optimize the widest boxes; ignore the thin ones.

Q75. [Medium] (Theory) Why is heavy work in build() a problem?

Hint

Runs every frame.

Solution

build() can run every frame, so sorting/parsing/formatting there repeats per rebuild. Compute once when data changes and cache; build() should only describe UI.

Q76. [Medium] (Coding) Rewrite efficiently and state the complexity change.

var s = '';
for (final n in nums) s += '$n,';
Hint

StringBuffer.

Solution
final b = StringBuffer();
for (final n in nums) b.write('$n,');
final s = b.toString();

+= is ~O(n²) copying; StringBuffer is O(n).

Q77. [Medium] (Coding) Why is this slow, and the fix?

final p = items.map(heavy);
print(p.length);
print(p.where((x) => x.ok).length);
Hint

Lazy re-iteration.

Solution

p is a lazy iterable; heavy re-runs on each iteration (here twice). Materialize once: final p = items.map(heavy).toList();.

Q78. [Medium] (Coding) Make this O(n).

final u = <String>[];
for (final s in input) if (!u.contains(s)) u.add(s);
Hint

Set.

Solution
final u = input.toSet().toList();

List.contains is O(n) → O(n²) loop; Set membership is O(1) average → O(n).

Q79. [Medium] (Theory) When does moving work to an isolate help, and when not?

Hint

CPU vs I/O.

Solution

Helps for CPU-bound work that blocks the UI isolate (parsing, image processing). Does nothing extra for pure I/O waiting, which async/await already handles without blocking the thread.

Q80. [Advanced] (Theory) Why prefer Uint8List over List<int> for big byte buffers?

Hint

Boxing + locality.

Solution

Uint8List is contiguous and unboxed — compact, cache-friendly, little GC churn. List<int> may box elements and is larger, hurting large numeric/byte workloads.

Q81. [Basic] (Theory) What's the right tool for a pure-Dart microbenchmark?

Hint

Warms up and averages.

Solution

package:benchmark_harness (compiled AOT), not a single Stopwatch read — it warms up and averages many runs.

Q82. [Medium] (Theory) How do you reduce excessive widget rebuilds?

Hint

Smallest subtree.

Solution

Rebuild the smallest subtree depending on the changed state: push setState down, use const to fence static subtrees, and watch the narrowest state slice with a state manager. DevTools' rebuild stats highlight offenders.

Q83. [Advanced] (Coding) Avoid the intermediate lists.

final a = nums.where((n) => n.isEven).toList();
final b = a.map((n) => n * n).toList();
Hint

Chain lazily, materialize once.

Solution
final b = nums.where((n) => n.isEven).map((n) => n * n).toList();

One pass, no throwaway intermediate list — less allocation/GC pressure.

Q84. [Medium] (Theory) Two equally-bad failure modes in performance work?

Hint

Premature vs none.

Solution

Premature optimization (complicating cold paths the profiler shows are irrelevant) and no optimization (shipping obvious O(n²)/per-frame parsing). The cure for both is profile-driven, targeted fixes.

Q85. [Advanced] (Theory) DevTools shows periodic GC pauses during scrolling. Likely cause and fix?

Hint

Allocation pressure.

Solution

Excess short-lived allocation per frame (Part 3) — e.g. non-const widgets, per-frame object/closure creation. Add const (Part 4), hoist invariants out of build(), and reuse buffers to cut churn.

Q86. [Advanced] (Theory) State the full optimization loop in one sentence.

Hint

Measure, fix widest, repeat.

Solution

Profile (in profile/release) → find the widest box → understand why (allocation, complexity, blocking, or rebuilds) → fix that one thing → profile again — and stop when under budget.


Section G — Metaprogramming & code generation (Q87–100)

Q87. [Basic] (Theory) What is metaprogramming?

Hint

Code about code.

Solution

Code that inspects or generates code — in Dart, mainly to eliminate boilerplate (serialization, data-class members, DI/routing, bindings) via build-time generation.

Q88. [Medium] (Theory) Why is dart:mirrors avoided in Flutter?

Hint

Tree shaking.

Solution

Runtime reflection defeats AOT tree-shaking (Part 2) — nothing can be proven unused — so it's unsupported in Flutter/AOT. Static codegen keeps the program tree-shakeable.

Q89. [Basic] (Theory) What do build_runner and source_gen do?

Hint

Orchestrator + helper layer.

Solution

build_runner orchestrates builders that read source and write generated files (cached, incremental). source_gen is a layer on top simplifying annotation-driven generators.

Q90. [Medium] (Coding) Name the three things needed to make json_serializable generate for a class.

Hint

Annotation, part, command.

Solution

@JsonSerializable() annotation, part 'x.g.dart'; directive, and dart run build_runner build.

Q91. [Medium] (Theory) Role of the part directive in codegen?

Hint

Private access.

Solution

It makes the generated file a part of your library, so generated members can access the library's private members (_$FooFromJson). You wire generated code in via part, never by editing it.

Q92. [Basic] (Coding) Command to fix "conflicting outputs" after a rename?

Hint

A --delete-... flag.

Solution
dart run build_runner build --delete-conflicting-outputs

Q93. [Medium] (Theory) Does code generation add runtime cost? Why?

Hint

Build-time only.

Solution

No. Generation runs at build time and emits ordinary .dart that's then compiled normally. At runtime there's no reflection or generation — just compiled code.

Q94. [Advanced] (Theory) How does a source_gen generator actually produce code?

Hint

Elements in, String out.

Solution

It extends GeneratorForAnnotation<T>, receives the analyzed program as Elements (analyzer package), inspects them, and returns Dart source as a String that build_runner writes out. It's "templating with type info."

Q95. [Medium] (Theory) When were Dart macros cancelled, and why?

Hint

Hot reload performance.

Solution

January 2025. Macros had to re-run during incremental compilation, slowing hot reload below Flutter's bar. The team couldn't get acceptable performance and shelved them indefinitely.

Q96. [Medium] (Theory) What is shipping instead of macros?

Hint

augment + better build_runner.

Solution

Augmentations (the augment keyword to split declarations across files) and a commitment to improving build_runner. build_runner remains the standard.

Q97. [Advanced] (Coding) What does an augmentation let a generator do that a part traditionally didn't?

Hint

Add to the same class.

Solution

augment class User { ... } lets generated code add members to the existing class (and fill in bodies) from another file, instead of only adding top-level _$... helpers glued via part. Cleaner generated APIs.

Q98. [Medium] (Theory) Name three popular packages that are build-time codegen and what they generate.

Hint

freezed, riverpod_generator, drift...

Solution

E.g. freezed (immutable data classes/unions/copyWith), riverpod_generator (providers from @riverpod), drift (SQL access), retrofit (HTTP clients), json_serializable (serialization).

Q99. [Advanced] (Theory) Tie the macros cancellation to the series' central theme.

Hint

Performance shapes features.

Solution

Macros conflicted with hot reload — the JIT-powered (Part 2) fast iteration that defines Flutter. Dart chose that performance over the feature. Performance constraints shape what Dart can even be — the through-line of the whole series.

Q100. [Advanced] (Theory) A teammate wants @JsonCodable()-style "macro magic" today with no build step. What do you tell them?

Hint

Macros aren't coming.

Solution

Macros were cancelled (Jan 2025) and aren't coming — there's no in-language no-build-step option. Use build_runner + json_serializable/freezed; it's the stable, blessed path, and augmentations are quietly improving the experience.


Coding Mini-Exercises

Ten larger, multi-concept problems. Try each before opening the solution. Exercise 10 is a capstone tying the whole series together.

Exercise 1 — Predict the build. Given the program below, list exactly which top-level functions survive into a release (AOT) build, and why.

void a() => b();
void b() => print('b');
void c() => print('c');
void main() => a();
Show solution

main, a, and b survive. main calls a, which calls b — all reachable. c is never reached from main, so tree shaking (Part 2) removes it.

Exercise 2 — Const audit. Refactor for maximum compile-time construction and explain each change.

Widget build(BuildContext context) {
  return Padding(
    padding: EdgeInsets.all(8),
    child: Column(children: [
      Text('Title', style: TextStyle(fontSize: 20)),
      SizedBox(height: 8),
      Text('Subtitle'),
    ]),
  );
}
Show solution
Widget build(BuildContext context) {
  return const Padding(
    padding: EdgeInsets.all(8),
    child: Column(children: [
      Text('Title', style: TextStyle(fontSize: 20)),
      SizedBox(height: 8),
      Text('Subtitle'),
    ]),
  );
}

The whole subtree is static, so one outer const makes the entire tree a canonicalized compile-time object: zero per-frame allocation and Flutter can skip rebuilding it (identity unchanged). const propagates to the inner EdgeInsets, TextStyle, SizedBox, and Text (Part 4).

Exercise 3 — Kill the leak. This screen leaks on every push/pop. Fix all issues.

class _FeedState extends State<Feed> {
  late final StreamSubscription _sub;
  late final AnimationController _anim;
  final _controller = ScrollController();

  @override
  void initState() {
    super.initState();
    _sub = feed.listen(_onItem);
    _anim = AnimationController(vsync: this);
  }
}
Show solution

Add a dispose that tears down everything with a lifecycle:

@override
void dispose() {
  _sub.cancel();
  _anim.dispose();
  _controller.dispose();
  super.dispose();
}

Each held StreamSubscription/AnimationController/ScrollController keeps the State (and its subtree) reachable until released (Part 3). Confirm with a DevTools heap snapshot that _FeedState instances no longer accumulate.

Exercise 4 — Lazy vs eager. This runs the expensive transform too many times. Fix it and explain.

Iterable<Report> reports() => raw.map(parseReport);

void summarize() {
  final r = reports();
  print('count: ${r.length}');
  print('failed: ${r.where((x) => x.failed).length}');
  print('first: ${r.first.id}');
}
Show solution

reports() returns a lazy iterable, so parseReport re-runs on every traversal — here three times. Materialize once:

void summarize() {
  final r = raw.map(parseReport).toList();
  print('count: ${r.length}');
  print('failed: ${r.where((x) => x.failed).length}');
  print('first: ${r.first.id}');
}

Now parseReport runs exactly once per item (Part 6).

Exercise 5 — Offload CPU work. A button parses a 20 MB JSON and freezes the UI. Rewrite it.

onPressed: () {
  final data = jsonDecode(hugeString); // ~1.5s on the UI isolate
  setState(() => _data = data);
}
Show solution
onPressed: () async {
  final data = await Isolate.run(() => jsonDecode(hugeString));
  setState(() => _data = data);
}

jsonDecode is CPU-bound; on the UI isolate it blocks the event loop for ~1.5 s, so frames can't paint (Part 2). Running it on a background isolate (or compute) keeps the UI thread free. async/await alone wouldn't help — there's nothing to wait on (Part 6).

Exercise 6 — FFI round trip. C exposes int sum_array(const int32_t* data, int32_t len);. Write Dart that allocates an array, fills it, calls the function, and frees memory — no leaks.

Show solution
import 'dart:ffi';
import 'package:ffi/ffi.dart';

typedef NativeSum = Int32 Function(Pointer<Int32>, Int32);
typedef DartSum = int Function(Pointer<Int32>, int);

int sumOf(DynamicLibrary lib, List<int> values) {
  final sumArray = lib.lookupFunction<NativeSum, DartSum>('sum_array');
  final buf = calloc<Int32>(values.length);
  try {
    for (var i = 0; i < values.length; i++) {
      buf[i] = values[i];
    }
    return sumArray(buf, values.length);
  } finally {
    calloc.free(buf); // native memory the GC won't reclaim ([Part 5])
  }
}

The try/finally guarantees the native buffer is freed even if the call throws (Part 5).

Exercise 7 — Complexity fix. Speed this up and give before/after complexity.

List<Order> withCustomer(List<Order> orders, List<Customer> customers) {
  return orders.where((o) =>
    customers.any((c) => c.id == o.customerId)).toList();
}
Show solution
List<Order> withCustomer(List<Order> orders, List<Customer> customers) {
  final ids = {for (final c in customers) c.id}; // Set, O(1) lookup
  return orders.where((o) => ids.contains(o.customerId)).toList();
}

The original is O(orders × customers) (a linear any scan per order). Building a Set of customer ids once makes membership O(1), giving O(orders + customers) (Part 6).

Exercise 8 — Codegen wiring. Show the minimal User class wired for json_serializable, and the command to generate.

Show solution
import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final String name;
  final int age;
  User(this.name, this.age);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
dart run build_runner build --delete-conflicting-outputs

Annotation + part + command — the universal codegen pattern (Part 7).

Exercise 9 — Explain the freeze chain. A profile-mode trace shows one 600 ms frame with a single wide box decodeAndResize() (pure Dart) on the UI thread. Walk through why it janks using the series' concepts, and give the fix.

Show solution

decodeAndResize() is synchronous CPU work on the UI isolate's single thread. While it runs, the event loop can't process the "paint frame" event, so ~36 frames are missed (600 ms ÷ ~16 ms) — visible jank (Part 2, Part 3). The flame chart's wide box pinpoints it (Part 6). Fix: move it to a background isolate (Isolate.run/compute), or, if it wraps a native codec, call that via FFI inside a worker isolate (Part 5). Re-profile to confirm the UI thread is clear.

Exercise 10 — Capstone: the high-performance importer. Design (in code sketch + prose) a feature that imports a large CSV from disk, parses it into immutable model objects, dedupes them, persists via a generated database layer, and never janks the UI. Identify which series concept each piece exercises.

Show solution
// 1. Immutable model with a const constructor (Part 4): zero-alloc reuse,
//    cheap equality, safe to share across isolates.
class Row {
  final String id;
  final int amount;
  const Row(this.id, this.amount);
}

// 2. Generated persistence layer (Part 7): build_runner + a package like
//    drift generates the type-safe DB code — no runtime reflection (Part 2),
//    so AOT tree-shaking stays intact.
//    @DriftDatabase(...) class AppDb ... → app_db.g.dart

Future<void> importCsv(String path, AppDb db) async {
  // 3. Read + parse on a BACKGROUND ISOLATE (Parts 2,3,6): CPU-bound parsing
  //    must not block the UI isolate, or frames stop painting.
  final rows = await Isolate.run(() {
    final text = File(path).readAsStringSync();      // I/O off the UI isolate too
    final buffer = <Row>[];                           // grows once
    for (final line in const LineSplitter().convert(text)) {
      final parts = line.split(',');
      buffer.add(Row(parts[0], int.parse(parts[1]))); // build models
    }
    // 4. Dedupe with a Set, O(n) not O(n²) (Part 6):
    final seen = <String>{};
    return [for (final r in buffer) if (seen.add(r.id)) r];
  });

  // 5. Persist via the generated layer. Batched insert keeps it efficient.
  await db.batchInsert(rows);
  // The worker's heap (with all the parse garbage) is GC'd independently of
  // the UI isolate (Part 3) — its allocation churn never touched our frames.
}

// 6. The UI just awaits and rebuilds the SMALLEST subtree (Part 6),
//    with const widgets fencing the static parts (Part 4).

Concept map:

  • Const modelsPart 4: immutable, canonicalizable, cheap equality, safe to pass between isolates.
  • Background isolate for parsing + I/OPart 2/Part 3/Part 6: CPU work off the UI thread so the event loop keeps painting; the worker's GC is independent.
  • Set-based dedupePart 6: O(n) membership instead of O(n²).
  • Generated DB layerPart 7: build_runner codegen, which exists because runtime reflection would break AOT tree-shaking (Part 2).
  • GC awarenessPart 3: the parse garbage lives and dies in the worker's heap.
  • Profile to verifyPart 6: confirm in profile mode that the UI thread stays under frame budget throughout the import.

If you can explain why each choice is the performant one, you've internalized the whole series.


You made it

A hundred questions, ten exercises, and a capstone — if you worked them honestly, you now understand not just how to write Dart, but what the machine does with it and how to make it fast:

  • Part 1 — The toolchain: CFE → kernel → two engines → snapshots → runtime.
  • Part 2 — JIT vs AOT: compile-while-running vs compile-before, hot reload, tree shaking.
  • Part 3 — GC & memory: generational collection, the scavenger, leaks via retained references.
  • Part 4 — const: compile-time construction, canonicalization, Flutter's free optimization.
  • Part 5 — FFI: calling C, types/structs, manual memory, callbacks.
  • Part 6 — Efficient Dart: profile first, read the flame chart, kill the anti-patterns.
  • Part 7 — Metaprogramming: build_runner, codegen, and the macros story.

This is the expert tier. Pair it with the Async & Concurrency series for the runtime's other half, and you can reason about Dart performance the way the framework authors do. Now go profile something. 🚀