AlanJereb.com
Coding

Flutter – Detecting memory leaks – Find leaks with Dart DevTools

Apr 12th, 2025

This is the third blog post in the

Flutter – Detecting memory leaks

series. If you just stumbled onto this article and want to start from the beginning, navigate

.

This article will cover the simplest and most suggested way of detecting Flutter memory leaks, which is using Dart DevTools Memory view tool. It is the topic extensively covered by Flutter’s documentation, blog posts and YouTube videos, but this series wouldn’t be complete without it.

Code setup

The best way to learn a new skill and retain the knowledge learned is by trying to do it yourself, not only reading about it. If your app doesn’t leak, and you are just learning about Flutter memory leak detection, I implore that you use my code setup to follow along. Otherwise use your leaking app.

The code below is a simple two-page Flutter application with a button that creates non-collectable timers.

import 'dart:async';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Memory Leaks demo app',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  void _induceMemoryLeak() {
    for (int i = 0; i < 100000; i++) {
      Timer.periodic(const Duration(seconds: 1), (timer) {
        // do nothing
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text("Memory Leaks demo page"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CustomTextButton(onPressed: _induceMemoryLeak, text: "Add 100.000 timers"),
            const SizedBox(height: 30),
            CustomTextButton(
              onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const SecondPage())),
              text: "Navigate",
            ),
          ],
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  const SecondPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text("Memory Leaks demo page - 2")),
      body: Center(
        child: CustomTextButton(
          onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const HomePage())),
          text: "Navigate back",
        ),
      ),
    );
  }
}

class CustomTextButton extends StatelessWidget {
  const CustomTextButton({super.key, required this.text, required this.onPressed});

  final String text;
  final Function() onPressed;

  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: TextButton.styleFrom(
        foregroundColor: Colors.white,
        backgroundColor: Colors.teal,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
      onPressed: onPressed,
      child: Text(text),
    );
  }
}

A two-page app with leaking timers

Detecting memory leaks with Dart DevTools

First things first. You must NEVER look for memory leaks running the Flutter app in debug mode, as the absolute memory used may be higher while running your application in debug mode. For more accurate results use

profile

mode.

Detection steps:

  • switch to the Memory view tab and select the Diff Snapshots tab
  • create a heap snapshot
  • use the application
  • create another heap snapshot
  • diff to find the leaking objects

Step 1: Switch to the memory view tab and select the Diff Snapshots tab

When your application is running in profile mode, open Dart DevTools and switch to the

Memory view

tab. This opens all of the memory-related tools Dart DevTools have to offer.

Next click on the

Diff Snapshots

tab. This tool lets you take memory heap snapshots throughout the application’s lifetime and compare the snapshots between them. If you remember from the

in this series, the leaking objects are not garbage collected, and in theory, you should catch them in here.

Dart DevTools - Memory view tab

Dart DevTools - Memory view tab

Step 2: Create a heap snapshot

As soon as your application stabilises, you need to create a memory heap snapshot. This will serve as a base for heap memory comparison.

Dart DevTools - How to create a memory heap snapshot

Dart DevTools - How to create a memory heap snapshot

Step 3: Use the application

As you are not really sure what is causing your memory leaks, this step is all about trying to induce memory leaks. You should use your app in the same way your end-users are/will use it. Something during its operation is causing your memory leaks. If you are following this article using my provided code, now it is time to press the

Add 100.000 timers

button and then the

Navigate

button to navigate to the next page. By returning to the original page, the garbage collector should have collected the timers if they were disposed of properly, but it didn’t.

Leaking application

Leaking application

Step 4: Create another heap snapshot

Now, your application is hopefully leaking, and it’s time to create a new memory heap snapshot that will be compared to the initial snapshot. Before making a snapshot, force the Garbage Collection by pressing the

GC

button inside the Dart DevTools Memory View tab.

Dart DevTools - Forcing garbage collection and making a new heap snapshot

Dart DevTools - Forcing garbage collection and making a new heap snapshot

Step 5: Diff to find the leaking objects

Before diffing the snapshots, make sure to set the class filters to show you all types of objects, only in this way, you are “sure” to see all of the objects.

Dart DevTools - Filtering object types

Dart DevTools - Filtering object types

And finally, you compare your latest heap snapshot with the original one.

Dart DevTools - Diffing the snapshots

Dart DevTools - Diffing the snapshots

Observing the difference between the two snapshots, we can see that, in my case, the leak was caused, indeed, by non-garbage-collected timers. You can see the number of timers created, that none of them was released from the memory and that they newly occupy 7.6 MB of additional memory.

What to do if you’ve found the culprit?

If you were lucky enough to find your culprit/s, it is time to monitor the object/s and try to find the leak’s origin in your app. If you are using my code, you already know the origin, but in real life, the origin of leaks will be more tricky to find. Introducing the

Trace Instances

tab.

After you switch to the Trace Instances tab, you need to set filters for the object classes you want to observe and check the

Trace

checkbox so the actual tracing commences. You then need to use your application like you did on the

Diff Snapshots

tab between your two heap snapshots. When you are fairly certain the leak has been introduced, press the

Refresh

button.

Dart DevTools - Tracing the leaking classes

Dart DevTools - Tracing the leaking classes

When the delta of the class changes, it is time to start the analysis. Click on the class and then on the

Expand All

button to expand the traces. Read carefully through the call tree to find the origin of your leak.

Dart DevTools - Leaking timers origin found

Dart DevTools - Leaking timers origin found

Is your code still leaking?

The above tool is not omnipotent. Your app might still leak, and the tool didn’t help you find your source. This happens for various reasons, like:

  • Indirect Retentions – Objects held via closures, callbacks, or framework internals may not show clear retention paths.
  • Unclosed Resources – Forgotten StreamSubscriptions, AnimationControllers, or ScrollControllers can leak without apparent traces.
  • Static/Global References – Objects stored in static variables or global caches won’t appear as leaks—they’re intentionally retained but might be unintentional in your logic.
  • Third-Party plugins – Leaks in plugins or libraries may not be visible in your app’s instance tracking.
  • Complex Object Graphs – If an object is retained through multiple layers (for example, a mix of widgets, providers, and streams), Dart DevTools might not highlight the root cause.

Remember, if the above techniques didn’t help, your leak might be relatively subtle – time to put on your detective hat and try the next tool in my utility belt, the Dart’s Leak Tracker. The tool is still in development and not officially released, but I will show you how you can use it in

next article

of the series

(coming soon)

.