flutter_leak_radar_lint

pub.dev License: MIT

A custom_lint plugin that detects common Flutter/Dart memory-leak patterns at analysis time — in your IDE and in CI — before they reach a running app.


Rules

Rule Severity Auto-fix Description
undisposed_controller WARNING Yes A Flutter controller or FocusNode field is created in a State but never disposed in dispose().
uncancelled_subscription WARNING Yes A StreamSubscription field is never cancelled in dispose() or close().
uncancelled_timer WARNING Yes A Timer field is never cancelled in dispose() or close().
unclosed_stream_controller WARNING Yes A StreamController field is never closed in dispose() or close().
missing_remove_listener WARNING No addListener is called without a matching removeListener in the teardown.
bloc_uncancelled_subscription WARNING No A .listen() call inside a BlocBase constructor produces a subscription that is never cancelled.
discarded_listen_result WARNING No The StreamSubscription returned by a bare stream.listen() call is discarded and can never be cancelled.

Installation

Add the plugin as a dev dependency:

# pubspec.yaml
dev_dependencies:
  custom_lint: ^0.8.1
  flutter_leak_radar_lint: ^0.1.0

Enable it in your analysis options:

# analysis_options.yaml
analyzer:
  plugins:
    - custom_lint

Run the analyzer:

dart run custom_lint

Your IDE (VS Code with the Dart extension, IntelliJ / Android Studio) will also show findings inline as warnings once the plugin is enabled.


Auto-fixes

undisposed_controller, uncancelled_subscription, uncancelled_timer, and unclosed_stream_controller ship with IDE quick-fixes. The fix inserts the missing teardown call into the existing dispose() method, or synthesises a dispose() override if the class does not have one yet.

Auto-fix synthesis is limited to dispose() and other synchronous teardowns. When the detected teardown is close() — which returns Future<void> — no new method is synthesised because a naive synchronous body would be incorrect. Add the close() method manually, then re-run the quick-fix to insert the cancel call into the existing body.


Suppression

Suppress a rule on a single line:

// ignore: undisposed_controller
final _controller = TextEditingController();

Suppress a rule for an entire file:

// ignore_for_file: discarded_listen_result

Known limitations

Rules scan teardown method bodies directly. If teardown delegates to a private helper method, the lint cannot follow the call and may produce a false positive:

@override
void dispose() {
  _disposeAll(); // helper — lint cannot see inside this call
  super.dispose();
}

Suppress the false positive on the field declaration:

// ignore: uncancelled_subscription
StreamSubscription<int>? _sub;

Runtime companion

Pair this plugin with flutter_leak_radar to catch leaks that slip through static analysis at runtime — with a visual overlay, heap-growth tracking, and shareable reports.


License

MIT — see LICENSE.