flutter_leak_radar
On-device memory leak detector for Flutter. Tracks per-class heap growth using
VM service snapshots and catches precise object retention through WeakReference
and Finalizer. Works in debug and profile builds. Complete no-op in release —
enabled defaults to kDebugMode || kProfileMode, so no guard code or build
flavours are required.
Installation
dependencies:
flutter_leak_radar: ^0.2.0
Quick start
1. Initialise in main()
import 'package:flutter_leak_radar/flutter_leak_radar.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await LeakRadar.init(LeakRadarConfig.standard(
autoScan: AutoScan(onNavigation: true), // scan after every pop
));
runApp(const MyApp());
}
LeakRadarConfig.standard() enables the detector in debug and profile, and
watches the default suspect set (*State, *Bloc, *Controller, etc.).
2. Wire the navigator observer
MaterialApp(
navigatorObservers: [LeakRadar.navigatorObserver],
home: ...,
)
This triggers an automatic scan a short time after each navigation pop.
3. Add the overlay badge
home: LeakRadar.overlay(child: const HomeScreen()),
The draggable badge shows the current worst severity and finding count. Tap to
open LeakRadarScreen. Long-press to trigger a manual scan immediately.
4. Open the results screen from anywhere
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LeakRadarScreen()),
);
Manual tracking
For types not covered by the default suspect set, opt in explicitly:
class MyService {
MyService() {
LeakRadar.track(this, tag: 'MyService');
}
void dispose() {
LeakRadar.markDisposed(this);
}
}
track registers the object with a WeakReference/Finalizer pair.
markDisposed tells the engine the object was intentionally released, suppressing
false positives.
Export and share
// Write a Markdown report to a temp file and get the path
final path = await LeakRadar.exportToFile(format: LeakExportFormat.markdown);
// Or export JSON
final jsonPath = await LeakRadar.exportToFile(format: LeakExportFormat.json);
LeakRadarScreen has built-in Export and Share buttons that call these APIs.
Configuration reference
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
kDebugMode || kProfileMode |
Master switch. No-op when false. |
autoScan |
AutoScan |
AutoScan() |
Periodic and/or navigation-triggered scan schedule. |
suspects |
SuspectSet |
SuspectSet.defaults() |
Which class-name patterns to track. |
rules |
List<LeakRule> |
[] |
Extra rules layered on top of suspects. |
maxSnapshots |
int |
20 |
Rolling history depth for growth analysis. |
gcCyclesForPreciseLeak |
int |
3 |
GC cycles before a tracked object is flagged as not-GCed. |
disposalGrace |
Duration |
2s |
Time after markDisposed before the object must be GCed. |
maxRetainingPathRequests |
int |
5 |
Max retaining-path fetches per scan (caps VM-service overhead). |
logLevel |
LeakLogLevel |
warning |
Internal log verbosity. |
showOverlay |
bool |
true |
Whether LeakRadar.overlay() renders the badge. |
AutoScan
AutoScan(
onNavigation: true, // scan on every didPop
period: const Duration(minutes: 2), // also scan periodically
navigationDebounce: const Duration(milliseconds: 500),
)
LeakRule glob patterns
LeakRule.growth('*Bloc') // flag any class ending in Bloc
LeakRule.maxLive('*Cache', 3) // flag if more than 3 Cache instances live
LeakRule.ignore('*Mock*') // never flag classes containing Mock
Glob: *X = ends with X, X* = starts with X, *X* = contains X,
X = exact match.
Manual heap snapshot
Capture a full binary heap snapshot at any point during a debug or profile run:
final path = await LeakRadar.captureHeapSnapshotToFile();
if (path != null) {
print('Heap snapshot written to: $path');
}
The file is named leak_radar_heap_<timestamp>.data and is written to
Directory.systemTemp by default. Pass a directory argument to write
elsewhere:
final dir = Directory('/path/to/output');
final path = await LeakRadar.captureHeapSnapshotToFile(directory: dir);
The .data file is a dartheap binary snapshot that can be loaded into:
- Flutter DevTools — open the Memory tab, click Import, and select the file to inspect the object graph interactively.
No VM-service connection is required — the snapshot is written directly via
dart:developer's NativeRuntime.writeHeapSnapshotToFile. The method returns
null (never throws) when the platform does not support it (release builds,
web, non-standalone VM).
LeakRadarScreen also exposes a Collect heap snapshot button (memory chip
icon) in its app bar that writes the snapshot and offers a Share sheet to send
the file directly from the device.
On-device limitations (heap-growth & graph scans)
Precise tracking (notGced / notDisposed) is pure Dart — it relies only on
WeakReference and Finalizer, so it works everywhere on-device, including a
plain flutter run on a physical Android or iOS device.
Heap-growth analysis and the retaining-path graph scan need a heap source, which is one of:
- a reachable in-process VM service — available on desktop and on emulators/simulators; or
NativeRuntime.writeHeapSnapshotToFile— not supported on a physical Android/iOS app's embedded VM.
On a plain flutter run on a physical device neither is available, so only
precise findings appear there. To exercise all detectors, run the example on
macOS or an emulator (flutter run -d macos) and tap "Run leak
self-test" on the home screen — it drives the leak scenario in the live app
and prints a LEAK-RADAR-SUMMARY block (grouped by LeakKind, including empty
kinds) to the console. The self-test is plain app code — no integration_test
package and no androidx.test native dependency — so it also runs on a
physical device (showing precise findings, with verbose logs explaining why the
graph/growth paths are unavailable there).
For offline analysis, feed an exported .data snapshot to the leak_graph CLI.
Debug/profile-only guarantee
The engine starts only when kDebugMode || kProfileMode is true (via
LeakRadarConfig.standard). In release builds every call (init, scan,
track, markDisposed, overlay, navigatorObserver) is a synchronous no-op
that returns a safe default. Nothing is compiled out — no tree-shaking or build
configuration required.
Relation to Flutter DevTools and leak_tracker
flutter_leak_radar is complementary to the official tooling:
- Flutter DevTools memory panel — great for interactive inspection; requires a connected DevTools session. LeakRadar works in the field with no tooling attached and can share reports as files.
- leak_tracker — precise lifecycle enforcement for unit tests and debug widgets. LeakRadar adds heap-growth analysis, a visual overlay, and is designed for integration testing and staging dogfooding rather than CI.
Bundled DevTools extension
Installing flutter_leak_radar also makes a discoverable "Leak Radar"
DevTools extension available: open Flutter DevTools while connected to your app
and it appears as its own tab automatically — no separate install. It offers a
capture-list of heap dumps, a per-class histogram, retaining paths grouped by
closest root, and composable filters for exploring the object graph.
Static analysis companion
Add flutter_leak_radar_lint to catch
undisposed controllers, uncancelled subscriptions, and similar patterns at
edit time before they cause runtime leaks.
License
MIT — see LICENSE.
Libraries
- flutter_leak_radar
- On-device, zero-config memory-leak detector for Flutter.