Pillar 01 · Memory

Catch leaks twice — before they ship, and while they run.

A static custom_lint plugin stops leak-prone code at analysis time. A runtime detector inspects the live heap on a real device and flags classes that grow and never return. No mixins, no base classes.

7
lint rules + auto-fixes
0
mixins to implement
2
calls for precise opt-in
Leak findings
scan 14:32
ChatRoomController +48
NOT DISPOSED
VideoPlayerController +6
GROWTH
_PresenceSubscription +3
NOT DISPOSED
8 classes · 57 live Scan now ↻

Runtime detection

It tracks instance growth so you don't have to.

Radar inspects the live heap through the Dart VM service. It counts live instances per class across forced-GC captures and flags any class that keeps growing and never comes back down — screens, blocs, controllers, subscriptions, timers.

Zero-config

One init call. No mixins, no interfaces, nothing to implement on your own types.

Heap-based, not hooks

It reads the real object graph from the VM — so it sees what's actually retained, not what you remembered to instrument.

Precise opt-in

Want exactness on a specific object? track(obj) and markDisposed(obj) — two calls, optional.

Static prevention · custom_lint

Stop the classics before they ship.

Seven rules, each with an auto-fix, flag leak-prone code right in your editor — and they're false-positive-disciplined: they understand disposal inside if/try blocks, constructor-injected (externally-owned) fields, and local variables.

  • Undisposed controllers
  • Uncancelled StreamSubscription & Timer
  • Unclosed StreamController
  • Discarded .listen() results
  • Missing removeListener
  • Bloc subscriptions never cancelled in close()
chat_screen.dart
class _ChatScreenState extends State {
  final _controller = TextEditingController();

  // 🛑 leak_radar: never disposed.
  //    Quick fix → add dispose()
}
QUICK FIX
+ @override
+ void dispose() {
_controller.dispose();
super.dispose();
+ }

On-device UX

The signal comes to you.

Retaining paths, on demand

Lazily fetched so capture stays cheap. See exactly what holds the reference — gc-root → field → … → object, with source locations.

GC root · static
└─ _ChatService._activeRooms :41:7
└─ _GrowableList [3]
└─ ChatRoomController ← leaked

Triggers that fit your flow

A NavigatorObserver scans on screen-pop — the natural "this should be gone now" checkpoint.

Scan now Periodic On screen-pop
Draggable badge

Live worst-severity + count, floating over your app. Tap for findings; growth sparklines per class.

Full heap snapshot

Capture a real .dartheap to file for offline DevTools analysis.

Export & share

Findings as JSON or Markdown, shareable straight from the device.

In DevTools

Capture as many as you like. Diff any two.

When you want to go deep, the DevTools companion keeps a running list of heap snapshots — capture as many as you like, export any of them, and diff any two, ranking per-class growth by instances and bytes. Retaining paths come grouped by closest root, separating live-tree objects from the leak-prone ones, with a composable filter over class and library. Plus a full class histogram — or grab the same heap snapshot headlessly from the command line.

A 38.2 MB → B 46.7 MB Δ +8.5 MB
classΔ instΔ bytes
ChatRoomController+12+2.1 MB
_MessageModel+840+1.9 MB
Uint8List+36+1.6 MB
String-220-180 KB

Real code

The whole memory setup.

main.dart
import 'package:radarscope/radarscope.dart';

void main() {
  FlutterRadar.init(
    memory: MemoryConfig(
      autoScan: AutoScan.onScreenPop,
      threshold: Severity.warning,
    ),
    overlay: true,
  );
  runApp(const MyApp());
}