How to activate Flutter DevTools and how to check memory leaks?
#flutter#devtools#memory#debugging#performance
Answer
Overview
Flutter DevTools is the official debugging and profiling suite shipped with the Flutter SDK. Its Memory tab helps you detect and fix memory leaks by taking heap snapshots, tracking allocations, and comparing diffs.
How to Open DevTools
From VS Code
text1. Run your Flutter app (F5 or flutter run) 2. Open Command Palette: Cmd+Shift+P (Mac) / Ctrl+Shift+P (Windows) 3. Select "Dart: Open DevTools" or "Flutter: Open DevTools" 4. DevTools opens in your browser
From Android Studio / IntelliJ
text1. Run your Flutter app 2. Click "Open DevTools" button in Flutter Inspector toolbar 3. Or: View > Tool Windows > Flutter Inspector
From CLI
bash# Run app in profile mode (recommended for profiling) flutter run --profile # Or launch DevTools standalone dart devtools
Memory Tab Features
| Feature | What It Does |
|---|---|
| Memory Chart | Real-time heap usage, capacity, GC events, RSS |
| Profile Memory | Current allocation by class and memory type |
| Diff Snapshots | Compare two heap snapshots to find leaks |
| Trace Instances | Track allocation call trees for specific classes |
Step-by-Step Guide to Detect Memory Leaks
Step 1: Run in Profile Mode
bashflutter run --profile
Always use profile mode on a physical device for accurate memory readings.
Step 2: Open Memory Tab
Open DevTools > Navigate to the Memory tab.
Step 3: Take Baseline Snapshot
In the "Diff Snapshots" section, click Take Snapshot (Snapshot 1).
Step 4: Perform the Suspected Action
For example, navigate to a screen and back — repeat 5-10 times.
Step 5: Force Garbage Collection
Click the GC button (trash can icon) to force garbage collection.
Step 6: Take Second Snapshot
Click Take Snapshot again (Snapshot 2).
Step 7: Compare the Diff
- Look for objects whose count keeps increasing between snapshots
- Filter by class name (e.g., your widget class, controllers)
- If a widget/controller count grows after navigating away, it's leaking
Step 8: Inspect Retaining Paths
Click on a leaked object to see what is holding a reference to it and preventing garbage collection.
Common Causes of Memory Leaks
1. Undisposed Controllers
dart// ❌ BAD — Controllers stay in memory forever class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin { late final AnimationController _animController; late final TextEditingController _textController; late final ScrollController _scrollController; void initState() { super.initState(); _animController = AnimationController( vsync: this, duration: const Duration(seconds: 1), ); _textController = TextEditingController(); _scrollController = ScrollController(); } // Missing dispose()! }
dart// ✅ GOOD — All controllers disposed void dispose() { _animController.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); }
2. Uncancelled Stream Subscriptions
dart// ❌ BAD — Subscription keeps listening after widget removed class _MyWidgetState extends State<MyWidget> { late StreamSubscription<int> _sub; void initState() { super.initState(); _sub = myStream.listen((data) { setState(() { /* update */ }); }); } // Missing cancel! }
dart// ✅ GOOD — Subscription cancelled in dispose void dispose() { _sub.cancel(); super.dispose(); }
3. Closures Holding Large Object References
dart// ❌ BAD — Closure captures entire 50MB object final hugeData = HugeDataModel(); // 50MB final callback = () => print(hugeData.name); globalRegistry.register(callback); // hugeData can't be garbage collected!
dart// ✅ GOOD — Capture only what you need final hugeData = HugeDataModel(); final name = hugeData.name; // Extract small piece final callback = () => print(name); globalRegistry.register(callback); // hugeData can now be garbage collected
4. BuildContext in Long-Lived Closures
dart// ❌ BAD — context captured in closure beyond widget lifetime Widget build(BuildContext context) { longLivedService.setHandler(() { apply(Theme.of(context)); // context retained! }); return Container(); }
dart// ✅ GOOD — Extract the value, not the context Widget build(BuildContext context) { final theme = Theme.of(context); longLivedService.setHandler(() { apply(theme); // Only ThemeData retained, not context }); return Container(); }
5. Active Timers Not Cancelled
dart// ❌ BAD — Timer runs forever Timer.periodic(Duration(seconds: 1), (timer) { updateSomething(); }); // ✅ GOOD — Cancel in dispose late Timer _timer; void initState() { super.initState(); _timer = Timer.periodic(Duration(seconds: 1), (timer) { updateSomething(); }); } void dispose() { _timer.cancel(); super.dispose(); }
Memory Leak Detection Checklist
| Check | What to Look For |
|---|---|
| Navigate to screen and back 5x | Widget/State object count should NOT increase |
| Open and close dialogs/sheets | Controller count should return to original |
| Scroll through long lists | Memory should stabilize, not grow unbounded |
| Switch tabs repeatedly | Previous tab's objects should be freed |
| Force GC then snapshot | After GC, leaked objects still remain |
Rule of Thumb: If you create it in
, you must clean it up intextinitState(). Controllers, subscriptions, timers, and listeners all need explicit cleanup.textdispose()
Learn more at Flutter DevTools Memory View.