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
No — helperB 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 models — Part 4: immutable, canonicalizable, cheap equality, safe to pass between isolates.
- Background isolate for parsing + I/O — Part 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 dedupe — Part 6: O(n) membership instead of O(n²).
- Generated DB layer — Part 7:
build_runnercodegen, which exists because runtime reflection would break AOT tree-shaking (Part 2). - GC awareness — Part 3: the parse garbage lives and dies in the worker's heap.
- Profile to verify — Part 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. 🚀