What is memory leaks how memory leaks can happen in flutter and how to debug it and proactively how to fix it?
#memory-leaks#performance#debugging#optimization
Answer
Overview
A memory leak occurs when memory that is no longer needed is not released, causing the app to consume more and more memory until it crashes or becomes sluggish.
What Are Memory Leaks?
dart// Memory Leak Example class BadWidget extends StatefulWidget { _BadWidgetState createState() => _BadWidgetState(); } class _BadWidgetState extends State<BadWidget> { late Timer _timer; void initState() { super.initState(); _timer = Timer.periodic(Duration(seconds: 1), (_) { setState(() {}); }); // ❌ MEMORY LEAK: Timer never cancelled! } // Missing dispose() - Widget removed but Timer keeps running }
Common Causes of Memory Leaks in Flutter
1. Not Disposing Controllers
dart// ❌ BAD - Memory Leak class LeakyWidget extends StatefulWidget { _LeakyWidgetState createState() => _LeakyWidgetState(); } class _LeakyWidgetState extends State<LeakyWidget> { final TextEditingController _controller = TextEditingController(); final AnimationController _animController = AnimationController( vsync: this, duration: Duration(seconds: 1), ); // ❌ No dispose() method! } // ✅ GOOD - Proper Cleanup class GoodWidget extends StatefulWidget { _GoodWidgetState createState() => _GoodWidgetState(); } class _GoodWidgetState extends State<GoodWidget> with SingleTickerProviderStateMixin { late TextEditingController _controller; late AnimationController _animController; void initState() { super.initState(); _controller = TextEditingController(); _animController = AnimationController( vsync: this, duration: Duration(seconds: 1), ); } void dispose() { _controller.dispose(); // ✅ Clean up _animController.dispose(); // ✅ Clean up super.dispose(); } Widget build(BuildContext context) { return TextField(controller: _controller); } }
2. Not Cancelling Stream Subscriptions
dart// ❌ BAD class StreamLeakWidget extends StatefulWidget { _StreamLeakWidgetState createState() => _StreamLeakWidgetState(); } class _StreamLeakWidgetState extends State<StreamLeakWidget> { void initState() { super.initState(); // ❌ Subscription never cancelled Stream.periodic(Duration(seconds: 1)).listen((event) { setState(() {}); }); } } // ✅ GOOD class NoStreamLeakWidget extends StatefulWidget { _NoStreamLeakWidgetState createState() => _NoStreamLeakWidgetState(); } class _NoStreamLeakWidgetState extends State<NoStreamLeakWidget> { late StreamSubscription _subscription; void initState() { super.initState(); _subscription = Stream.periodic(Duration(seconds: 1)).listen((event) { if (mounted) setState(() {}); }); } void dispose() { _subscription.cancel(); // ✅ Cancel subscription super.dispose(); } Widget build(BuildContext context) => Container(); }
3. Not Removing Listeners
dart// ❌ BAD class ListenerLeakWidget extends StatefulWidget { final ScrollController scrollController; const ListenerLeakWidget({required this.scrollController}); _ListenerLeakWidgetState createState() => _ListenerLeakWidgetState(); } class _ListenerLeakWidgetState extends State<ListenerLeakWidget> { void _scrollListener() { print('Scrolling...'); } void initState() { super.initState(); widget.scrollController.addListener(_scrollListener); // ❌ Listener never removed } } // ✅ GOOD class NoListenerLeakWidget extends StatefulWidget { final ScrollController scrollController; const NoListenerLeakWidget({required this.scrollController}); _NoListenerLeakWidgetState createState() => _NoListenerLeakWidgetState(); } class _NoListenerLeakWidgetState extends State<NoListenerLeakWidget> { void _scrollListener() { print('Scrolling...'); } void initState() { super.initState(); widget.scrollController.addListener(_scrollListener); } void dispose() { widget.scrollController.removeListener(_scrollListener); // ✅ Remove super.dispose(); } Widget build(BuildContext context) => Container(); }
4. Static/Global References
dart// ❌ BAD - Static reference prevents garbage collection class DataService { static Widget? cachedWidget; // ❌ Holds reference forever static BuildContext? context; // ❌ Very dangerous! static void cacheWidget(Widget widget) { cachedWidget = widget; // This widget can never be GC'd } } // ✅ GOOD - Use weak references or clear references class BetterDataService { Widget? _cachedWidget; // Instance variable, not static void cacheWidget(Widget widget) { _cachedWidget = widget; } void clear() { _cachedWidget = null; // ✅ Can be garbage collected } }
5. Timer Not Cancelled
dart// ❌ BAD class TimerLeakWidget extends StatefulWidget { _TimerLeakWidgetState createState() => _TimerLeakWidgetState(); } class _TimerLeakWidgetState extends State<TimerLeakWidget> { void initState() { super.initState(); Timer.periodic(Duration(seconds: 1), (_) { setState(() {}); // Runs forever! }); } } // ✅ GOOD class NoTimerLeakWidget extends StatefulWidget { _NoTimerLeakWidgetState createState() => _NoTimerLeakWidgetState(); } class _NoTimerLeakWidgetState extends State<NoTimerLeakWidget> { Timer? _timer; void initState() { super.initState(); _timer = Timer.periodic(Duration(seconds: 1), (_) { if (mounted) setState(() {}); }); } void dispose() { _timer?.cancel(); // ✅ Cancel timer super.dispose(); } Widget build(BuildContext context) => Container(); }
6. Image Cache Not Managed
dart// ❌ BAD - Loads huge images without limits ListView.builder( itemCount: 1000, itemBuilder: (context, index) { return Image.network('https://example.com/large-image-$index.jpg'); // ❌ All images stay in memory }, ); // ✅ GOOD - Use caching strategies ListView.builder( itemCount: 1000, itemBuilder: (context, index) { return CachedNetworkImage( imageUrl: 'https://example.com/large-image-$index.jpg maxHeightDiskCache: 1000, maxWidthDiskCache: 1000, memCacheHeight: 500, memCacheWidth: 500, ); }, ); // Or clear cache periodically void dispose() { imageCache.clear(); imageCache.clearLiveImages(); super.dispose(); }
How to Debug Memory Leaks
1. Using DevTools Memory Profiler
bash# Run app in profile mode flutter run --profile # Open DevTools flutter pub global activate devtools flutter pub global run devtools
Steps:
- Open DevTools → Memory tab
- Navigate through your app
- Take heap snapshot
- Navigate back/close widgets
- Force garbage collection (GC button)
- Take another snapshot
- Compare snapshots - memory should decrease
2. Memory Timeline
dart// Add this to see memory usage import 'dart:developer' as developer; void printMemoryUsage() { developer.Timeline.instantSync('Memory Usage', arguments: { 'rss': ProcessInfo.currentRss, 'heap': ProcessInfo.currentRss, }); } // Call periodically Timer.periodic(Duration(seconds: 5), (_) { printMemoryUsage(); });
3. Leak Detection with devtools_memory
yamldependencies: leak_tracker: ^10.0.0
dartimport 'package:leak_tracker/leak_tracker.dart'; void main() { LeakTracking.enable(); runApp(MyApp()); }
4. Manual Heap Dump Analysis
dart// Take heap snapshots programmatically import 'dart:developer'; Future<void> takeHeapSnapshot() async { final snapshot = await Service.getVM(); print('Heap size: ${snapshot.json!['heaps'][0]['size']}'); }
Proactive Fixes
1. Dispose Checklist
dartclass ProperWidget extends StatefulWidget { _ProperWidgetState createState() => _ProperWidgetState(); } class _ProperWidgetState extends State<ProperWidget> with SingleTickerProviderStateMixin { // ✅ Checklist of things to dispose late TextEditingController _textController; late ScrollController _scrollController; late AnimationController _animController; late StreamSubscription _subscription; Timer? _timer; void initState() { super.initState(); _textController = TextEditingController(); _scrollController = ScrollController(); _animController = AnimationController( vsync: this, duration: Duration(seconds: 1), ); _subscription = someStream.listen((data) {}); _timer = Timer.periodic(Duration(seconds: 1), (_) {}); } void dispose() { // ✅ Dispose EVERYTHING in reverse order _timer?.cancel(); _subscription.cancel(); _animController.dispose(); _scrollController.dispose(); _textController.dispose(); super.dispose(); // Always last! } Widget build(BuildContext context) => Container(); }
2. Use textmounted
Check
text
mounteddartFuture<void> fetchData() async { final data = await api.getData(); // ✅ Check if widget still mounted if (mounted) { setState(() { _data = data; }); } }
3. Weak References for Callbacks
dart// ❌ BAD - Strong reference in callback class DataService { Function? onDataReceived; void startListening(Function callback) { onDataReceived = callback; // Holds reference } } // ✅ GOOD - Use weak references or streams class BetterDataService { final _dataController = StreamController<Data>.broadcast(); Stream<Data> get dataStream => _dataController.stream; void startListening() { // Listeners auto-cleaned when subscription cancelled } void dispose() { _dataController.close(); } }
4. State Management Best Practices
dart// ✅ Riverpod - Auto-disposes final counterProvider = StateProvider<int>((ref) => 0); // ✅ BLoC - Remember to close class CounterBloc extends Bloc<CounterEvent, int> { Future<void> close() { // Clean up resources return super.close(); } } // ✅ GetX - Auto-disposes controllers class CounterController extends GetxController { void onClose() { // Clean up super.onClose(); } }
5. Image Optimization
dart// ✅ Limit cache size ImageCache().maximumSize = 100; ImageCache().maximumSizeBytes = 50 * 1024 * 1024; // 50 MB // ✅ Clear cache when needed void dispose() { imageCache.clear(); super.dispose(); } // ✅ Use appropriate image sizes Image.network( url, cacheHeight: 400, // Limit decoded size cacheWidth: 400, );
Memory Leak Detection Tools
| Tool | Purpose | Usage |
|---|---|---|
| DevTools Memory | Visual heap analysis | text |
| leak_tracker | Automatic leak detection | Add package, enable tracking |
| Timeline | Memory usage over time | text |
| Observatory | Low-level debugging | Built into Flutter |
Summary Checklist
✅ Always Dispose
- TextEditingController
- AnimationController
- ScrollController
- StreamSubscription
- Timer
- FocusNode
- ChangeNotifier
- Any custom controllers
✅ Best Practices
- Use check beforetext
mountedtextsetState() - Remove listeners in text
dispose() - Avoid static/global references to widgets
- Clear image cache periodically
- Use state management that auto-disposes
- Profile memory usage regularly
- Test navigation (back/forward) extensively
Golden Rule: For every
, there must be atextinit. For everytextdispose, there must be atextadd. For everytextremove, there must be atextstart.textstop