avoid_unremovable_callbacks_in_listeners
This rule flags a closure literal passed to addListener. removeListener matches by identity, and a closure creates a new object each time it is evaluated — so the listener can never be removed.
Why use this rule
Section titled “Why use this rule”removeListener(theClosure) compares object identity. A closure literal written at the call site is a different object from any closure you could later pass, so the removal silently does nothing and the listener stays registered.
Two consequences follow. The listener holds its captured scope — usually the whole State — alive for as long as the notifier lives, which is a genuine leak. And it keeps firing after disposal, so a setState inside it runs against a disposed element.
This pairs with always_remove_listener: that rule checks that a removal exists, this one checks that the removal can actually work.
See also: Flutter: ChangeNotifier.removeListener
Rebuilding on every keystroke by subscribing with an inline closure. The dispose() below looks complete, but the removeListener cannot match a closure it never saw:
class _SearchBarState extends State<SearchBar> { final _queryController = TextEditingController();
@override void initState() { super.initState(); _queryController.addListener(() => setState(() {})); }
@override void dispose() { _queryController.removeListener(() => setState(() {})); // a different object _queryController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}Give the callback a stable identity — a method reference is the same object every time it is named:
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();}A final field holding the closure works too, when the callback needs to capture something a method cannot reach:
class _RowState extends State<Row> { late final VoidCallback _onTick = () => setState(() => _ticks++); int _ticks = 0;
@override void initState() { super.initState(); widget.clock.addListener(_onTick); }
@override void dispose() { widget.clock.removeListener(_onTick); super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}Known limitations
Section titled “Known limitations”Matching is by method name, not by receiver type. Any single-argument call named addListener or addStatusListener that is given a closure literal is reported, whatever the receiver is. That keeps the rule working on your own Listenable implementations, but it also means an unrelated class with a method of that name is flagged — suppress those with // ignore: many_lints/avoid_unremovable_callbacks_in_listeners.
Only a closure literal written at the call site is reported. A variable, field, or method reference is assumed to have a stable identity and is left alone, even though a getter that builds a fresh closure on each read would have the same defect:
VoidCallback get _onTick => () => setState(() {}); // not reported, still unremovableA registration with more than one argument is skipped, since this rule is about the add/remove pair specifically. Add a project wrapper with additional_methods.
Turning this rule off
Section titled “Turning this rule off”This rule is in the recommended preset, so it is on with
preset: recommended or preset: opinionated.
To turn it off:
rules: avoid_unremovable_callbacks_in_listeners: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Options
Section titled “Options”many_lints: rules: avoid_unremovable_callbacks_in_listeners: additional_methods: [addObserver]rules: avoid_unremovable_callbacks_in_listeners: additional_methods: [addObserver]| Option | Type | Default | Description |
|---|---|---|---|
additional_methods |
list of strings | [] |
Extra registration methods whose counterpart removes by identity |
Related rules
Section titled “Related rules”always_remove_listener— Ensure every addListener() has a matching removeListener() in dispose().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.