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

text
1. 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

text
1. 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

FeatureWhat It Does
Memory ChartReal-time heap usage, capacity, GC events, RSS
Profile MemoryCurrent allocation by class and memory type
Diff SnapshotsCompare two heap snapshots to find leaks
Trace InstancesTrack allocation call trees for specific classes

Step-by-Step Guide to Detect Memory Leaks

Step 1: Run in Profile Mode

bash
flutter 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

CheckWhat to Look For
Navigate to screen and back 5xWidget/State object count should NOT increase
Open and close dialogs/sheetsController count should return to original
Scroll through long listsMemory should stabilize, not grow unbounded
Switch tabs repeatedlyPrevious tab's objects should be freed
Force GC then snapshotAfter GC, leaked objects still remain

Rule of Thumb: If you create it in

text
initState()
, you must clean it up in
text
dispose()
. Controllers, subscriptions, timers, and listeners all need explicit cleanup.

Learn more at Flutter DevTools Memory View.