always_remove_listener
Flags addListener() calls in State lifecycle methods (initState, didUpdateWidget, didChangeDependencies) that do not have a matching removeListener() call in dispose(). Missing removal causes memory leaks when the Listenable outlives the widget.
Why use this rule
Section titled “Why use this rule”Every addListener() on a ChangeNotifier, ValueNotifier, or AnimationController creates a strong reference to the callback. If the listener is not removed in dispose(), the callback (and everything it captures) stays in memory even after the widget is unmounted. This rule ensures every add has a matching remove with the same target and callback.
See also: ChangeNotifier | removeListener | Dart lint: cancel_subscriptions
Listening to a search field so the clear button can appear, and never unsubscribing. The State — and the whole subtree it captures — stays alive as long as the controller does:
class _SearchBarState extends State<SearchBar> { final _queryController = TextEditingController();
@override void initState() { super.initState(); _queryController.addListener(_onQueryChanged); // no matching removal }
void _onQueryChanged() => setState(() {});
@override Widget build(BuildContext context) => const SizedBox();}class _SearchBarState extends State<SearchBar> { final _queryController = TextEditingController();
@override void initState() { super.initState(); _queryController.addListener(_onQueryChanged); }
@override void dispose() { _queryController.removeListener(_onQueryChanged); _queryController.dispose(); super.dispose(); }
void _onQueryChanged() => setState(() {});
@override Widget build(BuildContext context) => const SizedBox();}Re-subscribing when the widget updates
Section titled “Re-subscribing when the widget updates”didUpdateWidget is checked too. Swapping notifiers means removing from the old one and adding to the new, and dispose() still needs its own removal:
class _MeterState extends State<Meter> { @override void didUpdateWidget(Meter oldWidget) { super.didUpdateWidget(oldWidget); if (widget.progress != oldWidget.progress) { oldWidget.progress.removeListener(_onProgress); widget.progress.addListener(_onProgress); } }
@override void dispose() { widget.progress.removeListener(_onProgress); super.dispose(); }
void _onProgress() => setState(() {});
@override Widget build(BuildContext context) => const SizedBox();}Known limitations
Section titled “Known limitations”Only initState, didUpdateWidget and didChangeDependencies are scanned. An addListener in build, in a callback, or in a helper method is not reported — registering a listener from those places is usually a separate bug, and this rule does not try to pair it.
The add and the remove are matched on the written form of both the target and the callback, so _queryController.addListener(_onQueryChanged) needs _queryController.removeListener(_onQueryChanged). Removing through a differently-spelled target, or via a helper method, is not recognised.
An inline closure passed to addListener can never be removed by identity at all; that is avoid_unremovable_callbacks_in_listeners’s job.
Turning this rule off
Section titled “Turning this rule off”This rule is in the core preset, so it is on with preset: core,
preset: recommended or preset: opinionated.
To turn it off:
rules: always_remove_listener: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Options
Section titled “Options”Projects with a state abstraction that does not extend Flutter’s State can
opt that base class into this rule:
many_lints: rules: always_remove_listener: state_base_classes: [AppState]rules: always_remove_listener: state_base_classes: [AppState]| Option | Type | Default | Description |
|---|---|---|---|
state_base_classes |
list of strings | [] |
Additional non-State base classes whose subclasses should be treated as state classes |
Related rules
Section titled “Related rules”avoid_unremovable_callbacks_in_listeners— Don’t pass an inline closure to addListener.dispose_fields— Ensure State fields with disposal methods are cleaned up in dispose().dispose_provided_instances— Ensure disposable instances in Riverpod providers are cleaned up with ref.onDispose.avoid_late_final_reassignment— Flag alate finalfield assigned twice on one path.