Question #395MediumPerformance & Optimization

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:

  1. Open DevTools → Memory tab
  2. Navigate through your app
  3. Take heap snapshot
  4. Navigate back/close widgets
  5. Force garbage collection (GC button)
  6. Take another snapshot
  7. 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

yaml
dependencies:
  leak_tracker: ^10.0.0
dart
import '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

dart
class 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
text
mounted
Check

dart
Future<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

ToolPurposeUsage
DevTools MemoryVisual heap analysis
text
flutter run --profile
leak_trackerAutomatic leak detectionAdd package, enable tracking
TimelineMemory usage over time
text
developer.Timeline
ObservatoryLow-level debuggingBuilt into Flutter

Summary Checklist

✅ Always Dispose

  • TextEditingController
  • AnimationController
  • ScrollController
  • StreamSubscription
  • Timer
  • FocusNode
  • ChangeNotifier
  • Any custom controllers

✅ Best Practices

  • Use
    text
    mounted
    check before
    text
    setState()
  • 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

text
init
, there must be a
text
dispose
. For every
text
add
, there must be a
text
remove
. For every
text
start
, there must be a
text
stop
.