always-remove-listener

Path
lint/correctness/always-remove-listener
Default
On (recommended)
Origin
Falcon
Domain
flutter
Rule
7 of 512
recommendedflutter

Requires Flutter Listenable registrations in State to be removed.

Receiver and callback must match: removing a different callback or listening to a different object does not clean up the registration. Disposing the same notifier also removes its listeners. Anonymous callbacks cannot be paired with a different closure. Unresolved APIs and non-State classes are ignored; local declaration spans distinguish shadowed receivers and callbacks. Unresolved expression receivers (including temporary derived animation views) are ignored rather than attributing their listeners to an unrelated field. Local aliases stored in fields and elements of the same iterated collection share receiver identity. Untraceable or ambiguous local ownership is ignored. Unknown assignments, compound writes, and reused loop variables invalidate local ownership. Null assertions do not change a receiver's identity.

Invalid

example.dartdart
import 'package:flutter/material.dart';
class A extends State<StatefulWidget> {
 final a = ValueNotifier(0);
 final b = ValueNotifier(0);
 final c = ValueNotifier(0);
 final d = ValueNotifier(0);
 final e = ValueNotifier(0);
 void listener() {}
 void wrong() {}
 void initState() {
   a.addListener(listener);
   b.addListener(listener);
   c.addListener(listener);
   d.addListener(() {});
   e.addListener(listener);
   e..addListener(wrong);
 }
 void dispose() { b.removeListener(wrong); c.removeListener(wrong); super.dispose(); }
}
class Borrowed extends StatefulWidget {
 final Animation<double> animation;
 final ValueListenable<int> value;
 Borrowed(this.animation, this.value);
 State<Borrowed> createState() => BorrowedState();
}
class BorrowedState extends State<Borrowed> {
 void tick() {}
 void initState() {
   widget.animation.addListener(tick);
   widget.value.addListener(tick);
 }
}

class CollectionListeners extends State<StatefulWidget> {
 final Map<String, ValueNotifier<int>> _amounts = {};
 final Map<String, ValueNotifier<int>> _other = {};
 void changed() {}
 void wrong() {}
 void initState() {
   for (final ValueNotifier<int> c in _amounts.values) {
     c.addListener(changed);
   }
 }
 void dispose() {
   for (final ValueNotifier<int> c in _other.values) {
     c.removeListener(changed);
     c.dispose();
   }
   for (final ValueNotifier<int> c in _amounts.values) {
     c.removeListener(wrong);
   }
   super.dispose();
 }
}

class StoredAliasWrongCleanup extends State<StatefulWidget> {
 ValueNotifier<int>? _authNotifier;
 final other = ValueNotifier<int>(0);
 void changed() {}
 void wrong() {}
 void initState() {
   final notifier = ValueNotifier<int>(0);
   _authNotifier = notifier;
   notifier.addListener(changed);
 }
 void dispose() {
   _authNotifier?.removeListener(wrong);
   other.dispose();
   super.dispose();
 }
}

class StoredAliasDifferentDisposedField extends State<StatefulWidget> {
 ValueNotifier<int>? _authNotifier;
 final other = ValueNotifier<int>(0);
 void changed() {}
 void initState() {
   final notifier = ValueNotifier<int>(0);
   _authNotifier = notifier;
   notifier.addListener(changed);
 }
 void dispose() {
   other.dispose();
   super.dispose();
 }
}
Remove this listener with the same callback when the `State` is disposed.
10 void initState() {
11 a.addListener(listener);
Remove this listener with the same callback when the `State` is disposed.
11 a.addListener(listener);
12 b.addListener(listener);
Remove this listener with the same callback when the `State` is disposed.
12 b.addListener(listener);
13 c.addListener(listener);
Remove this listener with the same callback when the `State` is disposed.
13 c.addListener(listener);
14 d.addListener(() {});
Remove this listener with the same callback when the `State` is disposed.
14 d.addListener(() {});
15 e.addListener(listener);
Remove this listener with the same callback when the `State` is disposed.
15 e.addListener(listener);
16 e..addListener(wrong);
Remove this listener with the same callback when the `State` is disposed.
28 void initState() {
29 widget.animation.addListener(tick);
Remove this listener with the same callback when the `State` is disposed.
29 widget.animation.addListener(tick);
30 widget.value.addListener(tick);
Remove this listener with the same callback when the `State` is disposed.
40 for (final ValueNotifier<int> c in _amounts.values) {
41 c.addListener(changed);
Remove this listener with the same callback when the `State` is disposed.
63 _authNotifier = notifier;
64 notifier.addListener(changed);
Remove this listener with the same callback when the `State` is disposed.
79 _authNotifier = notifier;
80 notifier.addListener(changed);

Valid

example.dartdart
import 'package:flutter/material.dart';
class A extends State<StatefulWidget> {
 final a = ValueNotifier(0);
 final b = ValueNotifier(0);
 final c = ValueNotifier(0);
 final d = ValueNotifier(0);
 final e = ValueNotifier(0);
 void listener() {}
 void initState() {
   a..addListener(listener);
   b.addListener(listener);
   this.c.addListener(this.listener);
   d.addListener(() {});
   e.addListener(listener);
 }
 void dispose() {
   a.removeListener(listener);
   b.dispose();
   c.removeListener(listener);
   d.dispose();
   e.removeListener(listener);
   super.dispose();
 }
}
class Other { final a = ValueNotifier(0); void run() { a.addListener(() {}); } }

class StoredLocalNotifier extends State<StatefulWidget> {
 ValueNotifier<int>? _authNotifier;
 void changed() {}
 void initState() {
   final notifier = ValueNotifier<int>(0);
   _authNotifier = notifier;
   notifier.addListener(changed);
 }
 void dispose() {
   _authNotifier?.dispose();
   super.dispose();
 }
}

class RemovedStoredAlias extends State<StatefulWidget> {
 ValueNotifier<int>? _authNotifier;
 void changed() {}
 void initState() {
   final notifier = ValueNotifier<int>(0);
   _authNotifier = notifier;
   notifier.addListener(changed);
 }
 void dispose() {
   _authNotifier?.removeListener(changed);
   super.dispose();
 }
}

class CollectionListeners extends State<StatefulWidget> {
 final Map<String, ValueNotifier<int>> _amounts = {};
 final Map<String, ValueNotifier<int>> _prices = {};
 void changed() {}
 void initState() {
   for (final c in _amounts.values) {
     c.addListener(changed);
   }
   for (final ValueNotifier<int> c in _amounts.values) {
     c.addListener(changed);
   }
   for (final c in _prices.values) {
     c.addListener(() {});
   }
   for (final ValueNotifier<int> c in _prices.values) {
     c.addListener(() {});
   }
 }
 void dispose() {
   for (final ValueNotifier<int> amount in _amounts.values) {
     amount.removeListener(changed);
   }
   for (final ValueNotifier<int> price in _prices.values) {
     price.dispose();
   }
   super.dispose();
 }
}

class UntraceableListenerOwner extends State<StatefulWidget> {
 void changed() {}
 void register(ValueNotifier<int> notifier) {
   notifier.addListener(changed);
 }
}

class AmbiguousListenerOwner extends State<StatefulWidget> {
 final first = ValueNotifier<int>(0);
 final second = ValueNotifier<int>(0);
 void changed() {}
 void register(bool condition) {
   var notifier = first;
   if (condition) notifier = second;
   notifier.addListener(changed);
 }
}

class AssertedStoredAlias extends State<StatefulWidget> {
 ValueNotifier<int>? _authNotifier;
 ValueNotifier<int>? _otherNotifier;
 void changed() {}
 void initState() {
   final notifier = ValueNotifier<int>(0);
   _authNotifier = notifier;
   notifier.addListener(changed);
   final other = ValueNotifier<int>(0);
   _otherNotifier = other;
   other.addListener(() {});
 }
 void dispose() {
   _authNotifier!.removeListener(changed);
   _otherNotifier!.dispose();
   super.dispose();
 }
}

class UntraceableWrites extends State<StatefulWidget> {
 final first = ValueNotifier<int>(0);
 final Map<String, ValueNotifier<int>> _amounts = {};
 void changed() {}
 void register(bool condition) {
   var indexed = first;
   indexed = _amounts['a']!;
   indexed.addListener(changed);
   var conditional = first;
   conditional = condition ? first : _amounts['b']!;
   conditional.addListener(changed);
   ValueNotifier<int>? compound = first;
   compound ??= _amounts['c']!;
   compound?.addListener(changed);
   var reused = first;
   for (reused in _amounts.values) {
     reused.addListener(changed);
   }
 }
}

Configure always-remove-listener

Set its severity in falcon.json. See the configuration guide.

falcon.jsonjson
{
  "linter": {
    "rules": {
      "correctness": {
        "always-remove-listener": "error"
      }
    }
  }
}

Or silence one occurrence with a suppression comment; the reason is required.

example.dartdart
// falcon-ignore lint/correctness/always-remove-listener: <reason>