How to read Flutter code you did not write
Every earlier module in this course taught you to write widgets, state, navigation, and platform code. This module teaches the other half: reading a StatefulWidget, a ConsumerWidget, a go_router config, or an entire feature someone — or something — else wrote, and deciding whether it is actually correct. An AI assistant hands you a screen that renders perfectly the first time you run it, and that success is the trap: the emulator shows the right pixels, flutter analyze is silent, and the demo works — while the code silently assumes a widget will never be disposed mid-flight, a list will never reorder, an isolate boundary will never see a BuildContext, or a route will never be visited before its sibling. Flutter’s tools validate almost nothing about behavior. flutter analyze proves types line up; it has no opinion on whether setState after an await is safe, whether a key is missing, or whether a ListView has anywhere to put its height.
Three things make this worse for Flutter specifically. First, Flutter and Dart genuinely changed shape mid-training-corpus: WillPopScope was deprecated for PopScope, StateNotifierProvider/StateProvider moved to Riverpod’s legacy.dart import, class modifiers and sealed classes changed how state unions get written, and material_ui/cupertino_ui started splitting off Material/Cupertino from package:flutter/material.dart — so a large fraction of “current” Flutter snippets online describe an API that compiles under an old SDK and throws an analyzer diagnostic under this course’s pinned Flutter 3.47.5 / Dart 3.13. Second, a huge share of public Flutter examples are demo-grade — a single screen tuned to “it renders once,” never a real app tuned for “it survives a rotation, a rebuild, and a disposed widget mid-await” — so patterns that are harmless in a 30-line gist (no mounted guard, no key, a Future built fresh in build()) get copied verbatim into code that cannot afford them. Third, the fastest fix an AI assistant reaches for after a layout crash is often the one closest at hand rather than the one that is correct — wrapping something in Expanded because the error message says the word, without checking there is a Flex for it to be a child of.
flowchart TD A["1. pubspec & SDK constraints<br/>which Flutter/Dart, which package versions does this assume?"] --> B["2. State approach<br/>Provider, Riverpod, Bloc, setState -- and is it wired up at all (ProviderScope)?"] B --> C["3. Widget tree shape<br/>keys on reordered children, const opportunities, GlobalKey use"] C --> D["4. Lifecycle & async gaps<br/>setState after dispose, setState during build, missing mounted"] D --> E["5. Layout constraints<br/>unbounded height, Expanded outside a Flex, overflow"] E --> F["6. Navigation<br/>go_router route order, go vs push, redirect + refreshListenable"] F --> G["7. Platform & isolates<br/>dart:io on web, MissingPluginException, compute() + BuildContext"] G --> H["8. Tests, lints & release config<br/>which lints actually run, print in release, --wasm, coverage"]
1. pubspec & SDK constraints
Section titled “1. pubspec & SDK constraints”Before anything about the code itself, check what SDK and package versions the code assumes — because Flutter’s own APIs (PopScope, material_ui, Riverpod’s legacy.dart split) and go_router’s route-matching behavior have all changed across versions this course targets directly, and a snippet written against an older SDK can still parse and even analyze cleanly, only to hit a real deprecation or missing-name diagnostic under 3.47.5.
- What to look for: does
pubspec.yaml’senvironment: sdk:constraint actually admit Dart 3.13 (or does it read like it was copied from a Dart 2 or early-Dart-3 project)? Does the code importpackage:flutter_riverpod/flutter_riverpod.dartand then callStateProvider/StateNotifierProviderdirectly — APIs that moved to a separatelegacy.dartimport in Riverpod 3? Does apubspec.yamlpincustom_lintandriverpod_linttogether without checking they actually resolve (they frequently do not — proven in Verification Tools)? - The question to ask: would this exact import, this exact named parameter, this exact class resolve on the SDK and package versions actually declared — or does it just look plausible because the shape of the API hasn’t changed even though the entry point has?
- Which lesson explains it: the AI Bug Catalog’s State & lifecycle group for the real
undefined_functiondiagnosticStateProviderproduces withoutlegacy.dart; Project and Tooling forpubspec.yamlstructure in full.
2. State approach
Section titled “2. State approach”Once the SDK is confirmed, check which state-management approach the code claims to use and whether it is actually wired up — Riverpod’s most common failure mode is not a wrong answer, it is a crash the instant a ConsumerWidget tries to read anything, because the one widget that makes state readable at all was never added.
- What to look for: is there a
ProviderScopeanywhere above the firstConsumerWidget/ref.watchcall? Does a.familyprovider have.autoDisposepaired with it (an unpaired.familyleaks one controller instance per parameter value, forever)? Isref.readcalled insidebuild()(silently non-reactive) orref.watchcalled inside a callback (throws)? Does the code reach forStateNotifierProvider/StateProviderfrom a Riverpod 2 tutorial when this course’s Riverpod 3 has moved both behind an explicitlegacy.dartimport? - The question to ask: if this exact widget is the very first
ConsumerWidgetbuilt afterrunApp(), does anything above it in the tree actually create theProviderContaineritsrefneeds — or does this only work by accident, because some ancestor screen happened to add the scope already? - Which lesson explains it: the AI Bug Catalog’s State & lifecycle group (missing
ProviderScope, legacyStateProvider) and Verification Tools for a realriverpod_lintmissing_provider_scopefinding; Provider and Riverpod for the mechanism in full.
3. Widget tree shape: keys and const
Section titled “3. Widget tree shape: keys and const”Once state is confirmed wired up, check the shape of the widget tree itself — Flutter matches widgets to their State by type and position unless told otherwise, and an AI assistant that has never had to debug a reordered list has no instinct for when that default silently attaches the wrong state to the wrong row.
- What to look for: does a list of stateful widgets built with
for/.map()have akeyat all, when its order can change (drag-to-reorder, sort, filter)? Is the sameGlobalKeyconstant reused across sibling widgets instead ofUniqueKey()/aValueKeyper item? Are compile-time-constant widgets actually markedconst— and does the project’s lint set even catch the ones that aren’t (flutter_lints6 does not ship the generalprefer_const_constructorsrule, only the narrower_in_immutablesvariant)? - The question to ask: if this exact list’s order changes on the next frame — a reorder, a delete, a filter — does each row’s own local state (a checkbox, an expanded/collapsed flag, a text field’s cursor) travel WITH its data, or does it stay pinned to a screen position and end up on the wrong row?
- Which lesson explains it: the AI Bug Catalog’s Widget tree & keys group, proven with a real reorder test and a real
Duplicate keys foundFlutterError; Keys for the mechanism in full.
4. Lifecycle & async gaps
Section titled “4. Lifecycle & async gaps”The single most common class of Flutter bug in AI-generated code is not a wrong answer at all — it is a correct answer computed after the widget asking for it is already gone, because nothing checked mounted between the await and the setState.
- What to look for: does every
setStatereachable from a.then(), anawaitcontinuation, aTimer, or aStreamlistener checkmountedfirst? Does a widget’sbuild()callsetStateon a different widget synchronously, from inside that widget’s ownbuild()(the “cross-sibling” trap)? Is alatefield ever read on a code path where the method that assigns it might not have run yet? - The question to ask: if the user navigates away, scrolls this widget out of a lazily-built list, or otherwise disposes this exact
Stateobject before this specific pendingFuture/Streamevent resolves, does anything stop the resulting callback from touching aStatethat no longer exists? - Which lesson explains it: the AI Bug Catalog’s State & lifecycle group, proven with real
setState() called after dispose()andsetState() or markNeedsBuild() called during builderrors captured fresh for this module; setState and Lifecycle for the full lifecycle order.
5. Layout constraints
Section titled “5. Layout constraints”Flutter’s layout crashes are almost all the same shape once you know the rule — an axis that wants to be as big as its content is placed somewhere that offers unlimited space in that same direction — and AI-generated code reaches for the first fix that mentions the right widget name (Expanded) without checking it has a Flex to be a child of.
- What to look for: is a
ListView/GridView/other scrollable placed directly inside aColumn/Rowwith nothing bounding its main-axis size? (The reverse — aColumninside aListView— needs nothing extra; a scrollable already gives its children a bounded cross-axis and unbounded main-axis, which is exactly what a naturally-sizedColumnwants.) IsExpanded/Flexibleused as a child of something that is not aRow/Column/Flex? Does aRowof fixed-width children have noExpanded/Flexible/scrolling escape hatch for content wider than the viewport? - The question to ask: walking from this exact widget up to the nearest ancestor that provides a bounded constraint in the axis that’s overflowing or crashing, is there one at all — and if this widget’s natural size ever exceeds what’s actually available, is there a flex factor, a scrollable, or a clip standing between them?
- Which lesson explains it: the AI Bug Catalog’s Layout group, proven with real
Vertical viewport was given unbounded height,Incorrect use of ParentDataWidget, andRenderFlex overflowederrors; Constraints and Layout for the constraints-down-sizes-up model in full.
6. Navigation
Section titled “6. Navigation”go_router’s route matching is declaration-order-sensitive in a way nothing in the type system catches, and its redirect callback only re-runs on navigation events by default — both are common places where AI-generated routing config compiles, runs, and quietly does the wrong thing.
- What to look for: is a parameterized route (
/pets/:id) declared before a more specific static route at the same segment level (/pets/new)? Iscontext.go()used where the user needs to be able to press back (it replaces the stack, not pushes onto it)? Does aredirectcallback read some external state (an auth flag) without the router’srefreshListenablepointed at whatever notifies when that state changes? - The question to ask: for this exact route table, in this exact declaration order, which route actually matches
/pets/newfirst — and if the value thisredirectreads changes without a navigation event happening at the same time, does anything tell the router to re-evaluate? - Which lesson explains it: the AI Bug Catalog’s Navigation group, proven with a real route-order misroute test, a real stack-depth comparison between
go/push, and a real stuck-on-loginredirecttest; Declarative Routing and Navigator and Routes for both mechanisms in full.
7. Platform & isolates
Section titled “7. Platform & isolates”Code that must run identically on mobile, desktop, and web, or that offloads work to a separate isolate, is where an AI assistant’s mental model of “one Dart, one runtime” breaks down most visibly — some of these bugs are compile-time, most are silent until a specific platform or a specific captured value is involved.
- What to look for: does
dart:io/Platform.isIOS/Fileget used unconditionally in code that also has to build for web (it compiles fine for web — every member just throwsUnsupportedErrorthe moment it actually runs)? Does aMethodChannel.invokeMethodcall assume a native handler is registered on every platform the app ships to? Does acompute()closure capture aBuildContext, aZone, or anything else tied to this isolate’s own binding (plain data and even some closures now cross the isolate boundary fine in current Dart — but anything reachable from the Element tree still cannot)? - The question to ask: if this exact function runs on the one platform (or the one isolate) it has never actually been tested on, does every value it touches still mean something there — or does it only work because every environment this was written and tested against happened to share an assumption the code never states?
- Which lesson explains it: the AI Bug Catalog’s Platform group, proven with a real
flutter build web --releasethat compilesdart:iocleanly, a realMissingPluginException, and a realIllegal argument in isolate message: object is unsendablecaptured for aBuildContext-capturingcompute()closure; Platform Channels and Isolates and compute() for both mechanisms in full.
8. Tests, lints & release config
Section titled “8. Tests, lints & release config”The last mile is checking whether the project’s own tooling would actually have caught any of the above — a green flutter analyze proves far less than it looks like it proves, and a print() statement that “obviously” gets stripped from a release build does not.
- What to look for: which lint package is actually included (
flutter_lints6 is narrower than its reputation — no generalprefer_const_constructors, for instance), and isriverpod_lint/custom_lintactually resolving together at the pinned versions (frequently not, as of this course’s own dependency check)? Does anything in the release pipeline stripprint()(nothing does, by default — it survives verbatim into a release web bundle, proven for this module)? Isflutter build web --wasmever exercised as a portability gate, or only the default JS-compiled build? - The question to ask: if this exact project’s CI ran nothing but
flutter analyzeand called it done, which of this module’s proven bugs would still have shipped — and does the project’sanalysis_options.yamlactually enable the rules its own README claims it does? - Which lesson explains it: the AI Bug Catalog’s Tooling & release group, proven with a real
flutter_lints6 lint-package diff and a realprint()string found verbatim inside a releasemain.dart.js; Verification Tools for the full ladder of tools and what each one actually proves; Testing, Build and Release, and Production Concerns for each topic in full.
What this module covers next
Section titled “What this module covers next”- The AI Bug Catalog — specific patterns, each with the code AI typically writes, the real output captured fresh for this module (an analyzer diagnostic, a
flutter testfailure, or a runtime error), and the fix. - Review Checklist — the same patterns as a one-page, eight-group checklist, in this reading order, each line linking the catalog entry and the tool that proves it.
- Verification Tools — the real ladder:
flutter analyze,analysis_options.yamlstrict modes,flutter_lints/riverpod_lint/custom_lint(with a real, current version-resolution finding),dart fix --dry-run,flutter test(--coverage, golden tests),flutter build web --wasmas a portability gate, DevTools, DartPad, and this course’s own harness. - Three worked reviews — Worked Review 1, Worked Review 2, Worked Review 3 — applying this reading order to full, AI-generated features (a Riverpod-codegen todo app with
go_router’sStatefulShellRoute, a media app spanning slivers/isolates/chunked upload, and a cross-platform app spanningdart:io/web, a platform channel, and a release build). These lessons are written and maintained separately from this one; if a link below 404s while that work is still in progress, the reading order and the catalog above stand on their own regardless. - Every earlier Phase 3 lesson in this course already has its own
## Reading AI-generated codesection with a<SpotTheBug>— this module’s catalog does not repeat those, it groups a new, broader set of patterns around the same reading order. Start from the course index if you want the full lesson-by-lesson path instead.
Mental Model
Section titled “Mental Model”Reading order for Flutter code you did not write:
1. pubspec & SDK constraints -> which Flutter/Dart, which package versions?2. State approach -> Provider/Riverpod/Bloc -- wired up at all?3. Widget tree shape -> keys on reordered children, const, GlobalKey4. Lifecycle & async gaps -> setState after dispose, setState during build5. Layout constraints -> unbounded height, Expanded outside a Flex6. Navigation -> go_router route order, go vs push, redirect7. Platform & isolates -> dart:io on web, MissingPluginException, compute()8. Tests, lints & release -> which lints actually run, print in release, --wasm
"It renders fine on the emulator" answers almost none of these -- most of thismodule's catalog entries render cleanly and break on rotate, rebuild, reorder,navigate, or release instead.Common Misconceptions
Section titled “Common Misconceptions”- “If
flutter analyzeis clean, the code is correct.” Proven throughout this module’s catalog: aConsumerWidgetwith noProviderScopeabove it, aListViewabout to overflow its constraints, and acompute()closure about to fail at runtime all analyze perfectly cleanly —flutter analyzeproves types and imports resolve, never that a runtime invariant holds. - “It rendered correctly once, so the bug is fixed.” The opposite is closer to true for most of this module: a missing
mountedguard, a missing key, and a route-order mistake all render correctly on the very first pass — they only show their cost on a rotation, a reorder, a second navigation, or a widget disposed mid-flight. - “AI-generated Flutter code just needs a newer prompt.” The problem is not staleness alone —
StateProvidergenuinely still exists in Riverpod 3, just behind a different import; the code that fails is often technically describing a real, current API, just not the one it thinks it’s using. - “A silent success is safer than an error.” The opposite is closer to true in this catalog:
dart:ioon web compiles without a single warning and only throws the moment aPlatform.isIOScheck actually executes in a browser — a build that had simply refused to compile would have been the safer failure.