Flutter Debugging Guide
This skill provides a systematic approach to debugging Flutter applications, covering common error patterns, debugging tools, and best practices for efficient problem resolution.
Common Error Patterns
- RenderFlex Overflow Error
Symptoms:
-
Yellow and black stripes appear in the UI indicating overflow area
-
Error message: "A RenderFlex overflowed by X pixels"
Causes:
-
Content exceeds available space in Row/Column
-
Fixed-size widgets in constrained containers
-
Text without proper overflow handling
Solutions:
// Solution 1: Wrap in SingleChildScrollView SingleChildScrollView( child: Column( children: [...], ), )
// Solution 2: Use Flexible or Expanded Row( children: [ Expanded( child: Text('Long text that might overflow...'), ), ], )
// Solution 3: Handle text overflow explicitly Text( 'Long text...', overflow: TextOverflow.ellipsis, maxLines: 2, )
- setState() Called After Dispose
Symptoms:
-
Runtime error: "setState() called after dispose()"
-
App crashes after async operations complete
Causes:
-
Calling setState() after widget is removed from tree
-
Async operations completing after navigation away
-
Timer callbacks on disposed widgets
Solutions:
// Solution 1: Check mounted before setState Future<void> fetchData() async { final data = await api.getData(); if (mounted) { setState(() { _data = data; }); } }
// Solution 2: Cancel async operations in dispose late final StreamSubscription _subscription;
@override void dispose() { _subscription.cancel(); super.dispose(); }
// Solution 3: Use CancelableOperation CancelableOperation<Data>? _operation;
Future<void> fetchData() async { _operation = CancelableOperation.fromFuture(api.getData()); final data = await _operation!.value; if (mounted) { setState(() => _data = data); } }
@override void dispose() { _operation?.cancel(); super.dispose(); }
- Null Check Operator Errors
Symptoms:
-
Error: "Null check operator used on a null value"
-
App crashes when accessing nullable values with !
Causes:
-
Using ! operator on null values
-
Not initializing late variables
-
Race conditions in async code
Solutions:
// Bad: Using ! without null check final name = user!.name;
// Good: Use null-aware operators final name = user?.name ?? 'Unknown';
// Good: Use if-null check if (user != null) { final name = user.name; }
// Good: Use pattern matching (Dart 3+) if (user case final u?) { final name = u.name; }
// For late initialization, consider nullable with check String? _data;
String get data { if (_data == null) { throw StateError('Data not initialized'); } return _data!; }
- Platform Channel Issues
Symptoms:
-
MissingPluginException
-
PlatformException with native code errors
-
Method channel not responding
Solutions:
// Solution 1: Ensure proper platform setup // Run flutter clean && flutter pub get
// Solution 2: Check method channel registration const platform = MethodChannel('com.example/channel');
try { final result = await platform.invokeMethod('methodName'); } on PlatformException catch (e) { debugPrint('Platform error: ${e.message}'); } on MissingPluginException { debugPrint('Plugin not registered for this platform'); }
// Solution 3: For plugins, ensure proper initialization void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize plugins here runApp(MyApp()); }
- Build Context Errors
Symptoms:
-
"Looking up a deactivated widget's ancestor is unsafe"
-
Navigator operations fail
-
Theme/MediaQuery not available
Causes:
-
Using context after widget disposal
-
Using context in initState before build
-
Storing context references
Solutions:
// Bad: Using context in initState @override void initState() { super.initState(); // Theme.of(context); // Error! }
// Good: Use didChangeDependencies @override void didChangeDependencies() { super.didChangeDependencies(); final theme = Theme.of(context); }
// Good: Use Builder for nested contexts Scaffold( body: Builder( builder: (context) { // This context has access to Scaffold return ElevatedButton( onPressed: () { Scaffold.of(context).showSnackBar(...); }, child: Text('Show Snackbar'), ); }, ), )
// Good: Use GlobalKey for cross-widget access final scaffoldKey = GlobalKey<ScaffoldState>();
- Hot Reload Failures
Symptoms:
-
Changes not reflecting after save
-
App state lost unexpectedly
-
"Hot reload not available" message
Causes:
-
Changing app initialization code
-
Modifying const constructors
-
Native code changes
-
Global variable mutations
Solutions:
Solution 1: Hot restart instead of hot reload
Press 'R' (capital) in terminal or Shift+Cmd+\ in VS Code
Solution 2: Full restart
flutter run --no-hot
Solution 3: Clean build
flutter clean && flutter pub get && flutter run
Code patterns that require hot restart:
// Changes to main() require restart void main() { runApp(MyApp()); // Modification here needs restart }
// Changes to initState logic often need restart @override void initState() { super.initState(); _controller = AnimationController(...); // Changes here need restart }
// Const constructor changes need restart const MyWidget({super.key}); // Adding/removing const needs restart
- Vertical Viewport Given Unbounded Height
Symptoms:
-
Error: "Vertical viewport was given unbounded height"
-
ListView inside Column fails
Solutions:
// Bad: ListView in Column without constraints Column( children: [ ListView(...), // Error! ], )
// Good: Use Expanded Column( children: [ Expanded( child: ListView(...), ), ], )
// Good: Use shrinkWrap (for small lists only) Column( children: [ ListView( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), children: [...], ), ], )
// Good: Use SizedBox with fixed height Column( children: [ SizedBox( height: 200, child: ListView(...), ), ], )
- RenderBox Was Not Laid Out
Symptoms:
-
Error: "RenderBox was not laid out"
-
Widget fails to render
Solutions:
// Ensure parent provides constraints SizedBox( width: 200, height: 200, child: CustomPaint(...), )
// For intrinsic sizing IntrinsicHeight( child: Row( children: [ Container(color: Colors.red), Container(color: Colors.blue), ], ), )
- Incorrect Use of ParentDataWidget
Symptoms:
-
Error: "Incorrect use of ParentDataWidget"
-
Positioned/Expanded used incorrectly
Solutions:
// Bad: Positioned outside Stack Column( children: [ Positioned(...), // Error! ], )
// Good: Positioned inside Stack Stack( children: [ Positioned( top: 10, left: 10, child: Text('Hello'), ), ], )
// Bad: Expanded outside Flex widget Container( child: Expanded(...), // Error! )
// Good: Expanded inside Row/Column Row( children: [ Expanded(child: Text('Hello')), ], )
- Red/Grey Screen of Death
Symptoms:
-
Red screen in debug/profile mode
-
Grey screen in release mode
-
App appears frozen
Causes:
-
Uncaught exceptions
-
Rendering errors
-
Failed assertions
Solutions:
// Global error handling void main() { FlutterError.onError = (details) { FlutterError.presentError(details); // Log to crash reporting service crashReporter.recordFlutterError(details); };
PlatformDispatcher.instance.onError = (error, stack) { // Handle async errors crashReporter.recordError(error, stack); return true; };
runApp(MyApp()); }
// Custom error widget class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { ErrorWidget.builder = (FlutterErrorDetails details) { return CustomErrorWidget(details: details); }; return MaterialApp(...); } }
Debugging Tools
Flutter DevTools
The comprehensive suite for Flutter debugging:
Launch DevTools
flutter pub global activate devtools flutter pub global run devtools
Or access via VS Code Flutter extension
Key DevTools Features:
-
Widget Inspector: Examine widget tree, properties, and render objects
-
Performance View: Analyze frame rendering and jank
-
Memory View: Track allocations and detect memory leaks
-
Network View: Monitor HTTP requests
-
Logging View: View all debug output
-
CPU Profiler: Identify performance bottlenecks
flutter doctor
Diagnose environment issues:
Full diagnostic
flutter doctor -v
Check specific issues
flutter doctor --android-licenses
Common fixes
flutter clean flutter pub get flutter pub upgrade
Dart DevTools Debugger
// Set breakpoints in code debugger(when: condition);
// Conditional breakpoints in IDE // Right-click line number > Add Conditional Breakpoint
Logging Best Practices
// Basic logging print('Debug message'); // stdout
// Better: debugPrint for large outputs (prevents dropped logs) debugPrint('Large debug output...');
// Best: dart:developer log for granular control import 'dart:developer';
log( 'User action', name: 'UserFlow', error: exception, stackTrace: stackTrace, );
// Conditional logging assert(() { debugPrint('Only in debug mode'); return true; }());
// Using kDebugMode import 'package:flutter/foundation.dart';
if (kDebugMode) { print('Debug only'); }
Flutter Inspector (Widget Inspector)
// Enable debug painting import 'package:flutter/rendering.dart';
debugPaintSizeEnabled = true; debugPaintBaselinesEnabled = true; debugPaintLayerBordersEnabled = true; debugPaintPointersEnabled = true;
// In widget debugPrint(context.widget.toStringDeep());
The Four Phases of Flutter Debugging
Phase 1: Identify the Error Type
Categorize the error:
-
Compile-time errors: Syntax, type errors (red squiggles)
-
Runtime errors: Exceptions during execution
-
Layout errors: RenderFlex overflow, unbounded constraints
-
State errors: setState after dispose, inconsistent state
-
Platform errors: Native plugin issues, permissions
-
Performance issues: Jank, memory leaks, slow frames
Get detailed error information
flutter analyze flutter test --reporter expanded
Phase 2: Reproduce and Isolate
// Create minimal reproduction class DebugWidget extends StatelessWidget { @override Widget build(BuildContext context) { // Isolate the problematic widget return Container( child: ProblematicWidget(), ); } }
// Use assertions to validate state assert(data != null, 'Data should not be null at this point'); assert(index >= 0 && index < list.length, 'Index out of bounds: $index');
Phase 3: Debug and Fix
// Add strategic logging void processData() { debugPrint('processData called with: $data');
try { final result = transform(data); debugPrint('Transform result: $result'); } catch (e, stack) { debugPrint('Transform failed: $e'); debugPrint('Stack trace: $stack'); rethrow; } }
// Use DevTools breakpoints // Set breakpoints at: // - Method entry points // - Before suspected failure points // - In catch blocks
Phase 4: Verify and Prevent
// Add tests for the fix testWidgets('Widget handles null data gracefully', (tester) async { await tester.pumpWidget( MaterialApp( home: MyWidget(data: null), ), );
expect(find.text('No data'), findsOneWidget); expect(tester.takeException(), isNull); });
// Add defensive coding Widget build(BuildContext context) { if (data == null) { return const EmptyState(); } return DataDisplay(data: data!); }
Quick Reference Commands
Diagnostics
Environment check
flutter doctor -v
Analyze code for issues
flutter analyze
Check for outdated packages
flutter pub outdated
Upgrade packages
flutter pub upgrade --major-versions
Testing
Run all tests
flutter test
Run specific test file
flutter test test/widget_test.dart
Run with coverage
flutter test --coverage
Run integration tests
flutter test integration_test/
Build and Run
Debug mode (default)
flutter run
Profile mode (for performance debugging)
flutter run --profile
Release mode
flutter run --release
Specific device
flutter run -d <device_id>
List devices
flutter devices
Clean and Reset
Clean build artifacts
flutter clean
Get dependencies
flutter pub get
Reset iOS pods
cd ios && pod deintegrate && pod install && cd ..
Reset Android
cd android && ./gradlew clean && cd ..
DevTools
Install DevTools globally
flutter pub global activate devtools
Run DevTools
flutter pub global run devtools
Or use dart devtools
dart devtools
Performance Debugging
Identify Jank
// Enable performance overlay MaterialApp( showPerformanceOverlay: true, ... )
// Or toggle in DevTools // Performance > Show Performance Overlay
Common Performance Issues
// Bad: Building expensive widgets in build() @override Widget build(BuildContext context) { final expensiveData = computeExpensiveData(); // Called every rebuild! return ExpensiveWidget(data: expensiveData); }
// Good: Cache expensive computations late final expensiveData = computeExpensiveData();
// Good: Use const constructors const MyWidget(key: Key('my-widget'));
// Good: Use RepaintBoundary for isolated repaints RepaintBoundary( child: ExpensiveAnimatedWidget(), )
Memory Debugging
// Check for leaks in DevTools Memory view
// Common leak patterns: // 1. Streams not disposed @override void dispose() { _streamSubscription.cancel(); super.dispose(); }
// 2. Controllers not disposed @override void dispose() { _textController.dispose(); _animationController.dispose(); super.dispose(); }
// 3. Listeners not removed @override void dispose() { _focusNode.removeListener(_onFocusChange); _focusNode.dispose(); super.dispose(); }
State Management Debugging
Provider/Riverpod
// Debug Provider rebuilds Consumer<MyModel>( builder: (context, model, child) { debugPrint('Consumer rebuilding: ${model.value}'); return Text(model.value); }, )
// Use ProviderScope observers ProviderScope( observers: [DebugProviderObserver()], child: MyApp(), )
class DebugProviderObserver extends ProviderObserver { @override void didUpdateProvider( ProviderBase provider, Object? previousValue, Object? newValue, ProviderContainer container, ) { debugPrint('Provider ${provider.name}: $previousValue -> $newValue'); } }
Bloc/Cubit
// Enable Bloc observer Bloc.observer = DebugBlocObserver();
class DebugBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); debugPrint('${bloc.runtimeType} $change'); }
@override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { debugPrint('${bloc.runtimeType} $error $stackTrace'); super.onError(bloc, error, stackTrace); } }
Platform-Specific Debugging
Android
View Android logs
flutter logs
Or use adb
adb logcat | grep flutter
Debug native crashes
adb logcat -s AndroidRuntime:E
iOS
View iOS logs
flutter logs
Open Xcode console
open ios/Runner.xcworkspace
Check crash logs
Xcode > Window > Devices and Simulators > View Device Logs
Web
// Use browser DevTools console import 'dart:html' as html;
html.window.console.log('Web debug message');
// Check for CORS issues in Network tab // Check for CSP issues in Console
Additional Resources
For detailed documentation:
-
Flutter Debugging Documentation
-
Flutter DevTools Guide
-
Common Flutter Errors
-
Error Handling in Flutter
-
Flutter Performance Profiling