Skip to content

Common Issues & Debugging


Diagnosing and resolving issues in Flutter applications requires systematic approaches and proper tooling. This guide covers common problems and effective strategies to identify, debug, and optimize your Flutter applications.


State management problems often manifest as inefficient UI updates, leading to performance issues or unexpected behaviors.

  1. Excessive Rebuilds - Too many widgets rebuilding when only a small part of the UI needs updating
  2. Missing Rebuilds - UI not updating when the underlying data changes
  3. Inconsistent State - Different parts of the UI showing inconsistent data
  4. Build Context Issues - Errors related to using BuildContext after widget disposal
Tracking Widget Rebuilds
// Add this import for the debugPrint function
import 'package:flutter/foundation.dart';
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Add debug print to track rebuilds
debugPrint('MyWidget rebuilding');
return Consumer<MyDataModel>(
builder: (context, model, child) {
// Add more specific rebuild tracking
debugPrint('MyWidget Consumer rebuilding with data: ${model.someValue}');
return Text(model.someValue);
},
);
}
}
Enabling Performance Overlay
// Enable performance overlay in your app
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
showPerformanceOverlay: true, // Shows performance metrics on screen
checkerboardRasterCacheImages: true, // Highlights cached images
checkerboardOffscreenLayers: true, // Highlights offscreen rendering
home: MyApp(),
),
);
}
Isolating State
// BAD: Rebuilding the entire screen when only a small part changes
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ExpensiveWidget(), // Rebuilds unnecessarily
Text('Count: $_counter'),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: Text('Increment'),
),
],
),
);
}
}
// GOOD: Isolate state to smallest possible widget
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ExpensiveWidget(), // No longer rebuilds
CounterWidget(),
],
),
);
}
}
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_counter'),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: Text('Increment'),
),
],
);
}
}

SelectionModel Pattern for Partial Updates

Section titled “SelectionModel Pattern for Partial Updates”
Selection Model Pattern
// Selection model pattern to avoid full list rebuilds
class SelectableListItem extends StatelessWidget {
final String title;
final String id;
final SelectionModel selectionModel;
const SelectableListItem({
required this.title,
required this.id,
required this.selectionModel,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: selectionModel.selectionNotifierFor(id),
builder: (context, isSelected, child) {
return ListTile(
title: Text(title),
selected: isSelected,
onTap: () => selectionModel.toggleSelection(id),
);
},
);
}
}
// Selection model that notifies only affected items
class SelectionModel extends ChangeNotifier {
final Set<String> _selectedIds = {};
final Map<String, ValueNotifier<bool>> _selectionNotifiers = {};
ValueNotifier<bool> selectionNotifierFor(String id) {
return _selectionNotifiers.putIfAbsent(
id,
() => ValueNotifier<bool>(_selectedIds.contains(id))
);
}
void toggleSelection(String id) {
if (_selectedIds.contains(id)) {
_selectedIds.remove(id);
} else {
_selectedIds.add(id);
}
// Only notify the specific item that changed
if (_selectionNotifiers.containsKey(id)) {
_selectionNotifiers[id]!.value = _selectedIds.contains(id);
}
notifyListeners();
}
}


Layout overflows occur when widgets attempt to render outside their allocated bounds, causing visual defects and warning messages.

Enabling Debug Painting
// Enable debug painting to see layout bounds
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show debugPaintSizeEnabled;
void main() {
debugPaintSizeEnabled = true; // Shows widget boundaries
runApp(MyApp());
}
Row Overflow Solutions
// PROBLEM: Text overflows in a fixed width Row
Row(
children: [
Icon(Icons.account_circle),
Text('This is a very long text that will overflow the available space'),
],
)
// SOLUTION 1: Use Expanded to allow the Text to take remaining space
Row(
children: [
Icon(Icons.account_circle),
Expanded(
child: Text('This is a very long text that will now wrap properly'),
),
],
)
// SOLUTION 2: Use Flexible with a tight fit
Row(
children: [
Icon(Icons.account_circle),
Flexible(
child: Text(
'This is a very long text that will wrap to multiple lines',
softWrap: true,
),
),
],
)

Using LayoutBuilder for Responsive Layouts

Section titled “Using LayoutBuilder for Responsive Layouts”
LayoutBuilder Example
// Use LayoutBuilder to adapt to available space
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth > 600) {
return WideLayout();
} else {
return NarrowLayout();
}
},
)
// Using LayoutBuilder to handle potential overflow
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final maxWidth = constraints.maxWidth;
return Container(
width: maxWidth,
child: Text(
'This text adapts to available width',
style: TextStyle(
fontSize: maxWidth > 300 ? 18.0 : 14.0,
),
overflow: TextOverflow.ellipsis,
maxLines: maxWidth > 200 ? 2 : 1,
),
);
},
)
Flex Space Allocation
// Using Flexible and Expanded to handle space allocation
Row(
children: [
// This will take 2/6 of the available space
Flexible(
flex: 2,
child: Container(
color: Colors.red,
height: 50,
),
),
// This will take 3/6 of the available space
Flexible(
flex: 3,
child: Container(
color: Colors.blue,
height: 50,
),
),
// This will take 1/6 of the available space
Flexible(
flex: 1,
child: Container(
color: Colors.green,
height: 50,
),
),
],
)

Flutter DevTools provides comprehensive profiling capabilities to identify performance bottlenecks.

  1. Install DevTools - flutter pub global activate devtools
  2. Run Your App in Debug/Profile Mode - flutter run --profile
  3. Connect DevTools - flutter pub global run devtools
  4. Connect to Your App - Enter the URL from the Flutter run command into DevTools
  1. Frame Rendering Time - Look for frames that take longer than 16ms (60fps) to render
  2. CPU Usage - Identify CPU-intensive operations that might be causing stutters
  3. Memory Usage - Monitor for memory leaks and excessive object allocations
  4. Widget Rebuild Count - Check for widgets that rebuild too frequently
Custom Rebuild Tracker
// Add a custom performance overlay to track rebuild times
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class RebuildTracker extends StatefulWidget {
final Widget child;
final String name;
const RebuildTracker({
required this.child,
required this.name,
Key? key,
}) : super(key: key);
@override
_RebuildTrackerState createState() => _RebuildTrackerState();
}
class _RebuildTrackerState extends State<RebuildTracker> {
int _buildCount = 0;
late final Stopwatch _stopwatch;
@override
void initState() {
super.initState();
_stopwatch = Stopwatch();
}
@override
Widget build(BuildContext context) {
_buildCount++;
_stopwatch.start();
final result = widget.child;
_stopwatch.stop();
if (_buildCount % 10 == 0 || _stopwatch.elapsedMilliseconds > 16) {
debugPrint(
'[PERF] ${widget.name} rebuilt $_buildCount times. '
'Last build took ${_stopwatch.elapsedMilliseconds}ms'
);
}
_stopwatch.reset();
return result;
}
}
Using the Rebuild Tracker
// Usage of the RebuildTracker
RebuildTracker(
name: 'ProductList',
child: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return RebuildTracker(
name: 'ProductItem $index',
child: ProductItem(product: products[index]),
);
},
),
)
Proper Resource Disposal
// Proper disposal of resources to prevent memory leaks
class _MyWidgetState extends State<MyWidget> {
late StreamSubscription _subscription;
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_subscription = stream.listen(_handleData);
}
@override
void dispose() {
// Always dispose resources when the widget is removed
_subscription.cancel();
_controller.dispose();
super.dispose();
}
}

Excessive rebuilds are one of the most common performance issues in Flutter applications.

Using Const Constructors
// Non-const widgets are recreated on each build
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.star),
);
}
// Const widgets are reused across builds
Widget build(BuildContext context) {
return const Container(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.star),
);
}
Optimizing Lists
// Using ListView.builder for efficient list rendering
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
// Only builds items that are visible
return ListTile(
title: Text(items[index].title),
subtitle: Text(items[index].description),
);
},
)
// Further optimization with cacheExtent
ListView.builder(
// Pre-build items beyond the visible area
cacheExtent: 200.0, // Default is 250.0
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].title),
);
},
)
// For complex lists, use constant keys to preserve state
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
// Using a key based on stable identifier helps Flutter reuse widgets
key: ValueKey(item.id),
title: Text(item.title),
);
},
)
Computed Properties
// Using computed properties to avoid recalculation
class ProductViewModel {
final List<Product> products;
ProductViewModel(this.products);
// Cache for expensive computation
double? _averagePrice;
// Computed property with memoization
double get averagePrice {
// Return cached value if available
_averagePrice ??= _calculateAveragePrice();
return _averagePrice!;
}
double _calculateAveragePrice() {
if (products.isEmpty) return 0.0;
// This could be expensive for large lists
final total = products.fold<double>(
0.0,
(sum, product) => sum + product.price,
);
return total / products.length;
}
// Reset cache when data changes
void updateProducts(List<Product> newProducts) {
products = newProducts;
_averagePrice = null; // Invalidate cache
}
}
Using Isolates for Heavy Processing
// Moving expensive work to an isolate
import 'dart:isolate';
import 'dart:async';
// Function to run in isolate
Future<List<String>> processDataInBackground(List<String> inputData) async {
// Create a ReceivePort for getting the result back
final receivePort = ReceivePort();
// Spawn isolate
await Isolate.spawn(
_processDataIsolate,
_IsolateData(receivePort.sendPort, inputData),
);
// Get the result
final result = await receivePort.first as List<String>;
return result;
}
// Data wrapper for sending to isolate
class _IsolateData {
final SendPort sendPort;
final List<String> data;
_IsolateData(this.sendPort, this.data);
}
// Function that runs in the isolate
void _processDataIsolate(_IsolateData isolateData) {
// Perform expensive computation
final result = isolateData.data.map((item) {
// Simulate complex processing
return item.toUpperCase();
}).toList();
// Send result back to the main isolate
isolateData.sendPort.send(result);
}
Compute Function
// Using compute for simpler isolate usage
import 'package:flutter/foundation.dart';
// This function will run in a separate isolate
List<String> _processData(List<String> data) {
return data.map((item) => item.toUpperCase()).toList();
}
class _MyWidgetState extends State<MyWidget> {
List<String> _processedData = [];
bool _isLoading = false;
Future<void> _loadData() async {
setState(() => _isLoading = true);
final rawData = await fetchRawData();
// Process in background thread
final processedData = await compute(_processData, rawData);
setState(() {
_processedData = processedData;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return _isLoading
? CircularProgressIndicator()
: ListView.builder(
itemCount: _processedData.length,
itemBuilder: (context, index) => ListTile(
title: Text(_processedData[index]),
),
);
}
}

  1. State Management
    • Isolate state to the smallest widgets possible
    • Use appropriate state management patterns for your app size
    • Leverage granular update mechanisms like ValueNotifier
  2. Widget Structure
    • Use const constructors for static widgets
    • Apply proper keys in lists and dynamic widget trees
    • Create logical rebuild boundaries to contain updates
  3. Layout
    • Handle overflow issues with Expanded, Flexible, and proper constraints
    • Use LayoutBuilder to adapt to available space
    • Apply appropriate overflow strategies for text (ellipsis, wrapping)
  4. Resource Management
    • Properly dispose of controllers, listeners, and other resources
    • Use lazy loading for expensive resources
    • Move heavy computations off the main thread using isolates
  5. Profiling & Monitoring
    • Regularly profile your app with DevTools
    • Add performance tracking in debug builds
    • Benchmark critical paths and user journeys

  1. Flutter DevTools Documentation – Official guide to Flutter’s profiling tools
  2. Flutter Performance Best Practices – Official performance recommendations
  3. Flutter Profiler – How to diagnose UI performance issues