Skip to content

avoid_unremovable_callbacks_in_listeners

v1.0.0WarningConfigurableResource Management

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.

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

controller.addListener(() => setState(() {})); // can never be removed

Give the callback a stable identity:

void _onChange() => setState(() {});
@override
void initState() {
super.initState();
controller.addListener(_onChange);
}
@override
void dispose() {
controller.removeListener(_onChange);
super.dispose();
}

Only addListener and addStatusListener are recognised by default; a project wrapper can be added with additional_methods.

A registration with more than one argument is skipped, since this rule is about the add/remove pair specifically.

This rule is in the recommended preset, so it is on with preset: recommended or preset: opinionated.

To turn it off:

many_lints.yaml
rules:
avoid_unremovable_callbacks_in_listeners: false

To keep the rule on but skip certain paths, use per-rule exclude.

analysis_options.yaml
many_lints:
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