Flutter Debugging
A repeatable protocol for diagnosing Flutter errors: classify → gather context → trace root cause → apply minimal fix → validate → prevent recurrence.
Rules: never rewrite because of a bug; never accept a workaround when the root cause is findable; state the root cause in one sentence before writing any code.
Step 1: Classify the Issue
| Error Pattern | Category | Primary Tool |
|---|---|---|
RenderFlex overflowed by N pixels |
Layout | Widget Inspector, wrap in Expanded/Flexible |
Null check operator used on a null value |
Null safety | Stack trace, find where ! is called on null |
StateError: Bad state: No element |
Collection/async | Provider lifecycle, stream state |
LateInitializationError |
Initialization | Trace where late field is used before initState |
type 'Null' is not a subtype of 'X' |
Type / JSON | DTO fromJson, null field in API response |
| Gradle build failure | Android build | Gradle logs, SDK/AGP version matrix |
| CocoaPods error | iOS build | pod install, Podfile.lock conflicts |
| Firebase initialization error | Config | google-services.json, firebase_options.dart |
MissingPluginException |
Plugin | flutter clean && flutter pub get, full rebuild |
PlatformException |
Native interop | Plugin version, gradle/pod sync |
ProviderException / ProviderDisposedException |
Riverpod lifecycle | autoDispose timing, keepAlive |
setState() called after dispose() |
Widget lifecycle | mounted check after await |
Step 2: Gather Context
Before attempting a fix:
- Full error message and stack trace (exact text, not paraphrased)
- Flutter version:
flutter --version - Platform: iOS / Android / Web
- Trigger: hot reload / cold start / specific action / after specific data
- Recent changes:
git diff
For build errors: flutter doctor -v
For runtime crashes: flutter run --verbose 2>&1 | head -200
Step 3: Root Cause by Category
RenderFlex Overflow
Open Widget Inspector, find the Row/Column in the stack trace.
Fixes in priority order:
Expanded(child: Text(longText))— fills available spaceFlexible(child: Text(longText))— shrinks but doesn't force fillSingleChildScrollView(scrollDirection: Axis.horizontal, child: Row(...))— for scrollable content
Null Safety Errors
Find the ! on the stack trace line. Trace backwards to when the variable is null.
final name = user?.displayName ?? 'Anonymous'; // null-aware
if (user == null) return; // early guard
Never use late unless the value is guaranteed set in initState before first build.
Riverpod / Provider Errors
ProviderDisposedException — provider with autoDispose accessed after widget unmount, or ref.read called in async method after await when provider was already disposed.
Fix: cache the value before await:
final repo = ref.read(repositoryProvider); // before await
await someAsyncOperation();
await repo.doSomething(); // use cached ref
For providers that must outlive the screen: @Riverpod(keepAlive: true).
Async / BuildContext Errors
setState() or Navigator.of(context) called after await when widget may be disposed.
await repository.save(data);
if (!mounted) return;
Navigator.of(context).pop();
Or capture navigator before the await: final navigator = Navigator.of(context);
Android / Gradle Build Failures
- Read the FULL output — the real error is before
BUILD FAILED. - Check
compileSdkVersion,targetSdkVersion,minSdkVersion,kotlinVersion. - Check Android Gradle Plugin (AGP) version compatibility.
flutter clean && flutter pub get
cd android && ./gradlew clean --refresh-dependencies
Common: D8: Invoke-customs only supported with --min-api 26 → raise minSdkVersion to 26.
Duplicate class kotlin.collections.jdk8 → Kotlin version to 1.8+.
iOS / CocoaPods Build Failures
cd ios && pod deintegrate && pod install
pod repo update && pod install # if repo is stale
If deployment target issue, update ios/Podfile: platform :ios, '14.0'
After any Podfile change: flutter clean && cd ios && pod install && cd .. && flutter run
Firebase Initialization Errors
Checklist:
google-services.jsoninandroid/app/(notandroid/).GoogleService-Info.plistinios/Runner/and added to Xcode target.await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)beforerunApp().firebase_options.dartgenerated viaflutterfire configure.- Package name in
google-services.jsonmatchesapplicationIdinbuild.gradle.
MissingPluginException
Plugin's native code not compiled into the app.
flutter clean && flutter pub get && flutter run # full rebuild required
Step 4: Apply the Minimal Fix
State the root cause in one sentence. Apply the smallest change that fixes it. Do not refactor surrounding code during a bug fix — that changes two things at once and makes the fix harder to verify.
Step 5: Validate
flutter analyze
flutter test
flutter clean && flutter pub get && flutter build [apk|ios|web] # for build errors
Never mark a bug fixed until flutter analyze passes and the relevant test passes.
If no test covers the bug, write one before closing.
Step 6: Prevent Recurrence
- Write a regression test that would have caught the bug.
- If the bug is from a pattern the team repeats, append to
context-log-gotchas.md. - If it required a non-obvious setup step, document in
docs/debugging/or the README.
Flutter Gotcha Reference
- Hot reload vs hot restart — hot reload preserves widget state; hot restart re-initializes everything. Changes to
main.dartinit, platform channels, and plugin registration require hot restart. constconstructors — mark every widgetconstwhen fields are compile-time constants.flutter analyzeflags missingconst.- Keys in lists — always provide
ValueKey(item.id)to list items that can reorder, be inserted, or removed. BuildContextacross async gaps — anyawaitcreates a gap; the widget may unmount before the code afterawaitruns. Checkmountedor capture imperative objects beforeawait.- Streams not closed —
StreamSubscriptionandStreamControllermust be cancelled/closed indispose(). Riverpod: useref.onDispose. autoDisposetiming — provider is destroyed when last listener is removed. UsekeepAlivefor data that must persist across navigation.freezedunion exhaustiveness — missingwhen/mapcase compiles but throws at runtime. UsemaybeWhenonly when you genuinely don't need all states.
Cross-references
- Error type hierarchy:
@references/dart-error-mapping.md - Agent:
flutter-debugger - Build config issues:
flutter-release-readinessskill (Android/iOS checklist)