| name | debug:flutter |
| description | Debug Flutter applications systematically with this comprehensive troubleshooting skill. Covers RenderFlex overflow errors, setState() after dispose() issues, null check operator failures, platform channel problems, build context errors, and hot reload failures. Provides structured four-phase debugging methodology with Flutter DevTools, widget inspector, performance profiling, and platform-specific debugging for Android, iOS, and web targets. |
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
1. 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,
)
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();
}
3. 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!;
}
4. 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());
}
5. 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>();
6. 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:
flutter run --no-hot
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
7. 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(...),
),
],
)
8. 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),
],
),
)
9. 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')),
],
)
10. 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:
flutter pub global activate devtools
flutter pub global run devtools
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:
flutter doctor -v
flutter doctor --android-licenses
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
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
flutter doctor -v
flutter analyze
flutter pub outdated
flutter pub upgrade --major-versions
Testing
flutter test
flutter test test/widget_test.dart
flutter test --coverage
flutter test integration_test/
Build and Run
flutter run
flutter run --profile
flutter run --release
flutter run -d <device_id>
flutter devices
Clean and Reset
flutter clean
flutter pub get
cd ios && pod deintegrate && pod install && cd ..
cd android && ./gradlew clean && cd ..
DevTools
flutter pub global activate devtools
flutter pub global run 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
flutter logs
adb logcat | grep flutter
adb logcat -s AndroidRuntime:E
iOS
flutter logs
open ios/Runner.xcworkspace
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: