Allow remove listener on disposed change notifier (#97988)
diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart
index 2775511..5f45282 100644
--- a/packages/flutter/lib/src/foundation/change_notifier.dart
+++ b/packages/flutter/lib/src/foundation/change_notifier.dart
@@ -103,7 +103,12 @@
/// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value.
class ChangeNotifier implements Listenable {
int _count = 0;
- List<VoidCallback?> _listeners = List<VoidCallback?>.filled(0, null);
+ // The _listeners is intentionally set to a fixed-length _GrowableList instead
+ // of const [] for performance reasons.
+ // See https://github.com/flutter/flutter/pull/71947/files#r545722476 for
+ // more details.
+ static final List<VoidCallback?> _emptyListeners = List<VoidCallback?>.filled(0, null);
+ List<VoidCallback?> _listeners = _emptyListeners;
int _notificationCallStackDepth = 0;
int _reentrantlyRemovedListeners = 0;
bool _debugDisposed = false;
@@ -220,7 +225,7 @@
///
/// If the given listener is not registered, the call is ignored.
///
- /// This method must not be called after [dispose] has been called.
+ /// This method returns immediately if [dispose] has been called.
///
/// {@macro flutter.foundation.ChangeNotifier.addListener}
///
@@ -230,7 +235,11 @@
/// changes.
@override
void removeListener(VoidCallback listener) {
- assert(_debugAssertNotDisposed());
+ // This method is allowed to be called on disposed instances for usability
+ // reasons. Due to how our frame scheduling logic between render objects and
+ // overlays, it is common that the owner of this instance would be disposed a
+ // frame earlier than the listeners. Allowing calls to this method after it
+ // is disposed makes it easier for listeners to properly clean up.
for (int i = 0; i < _count; i++) {
final VoidCallback? listenerAtIndex = _listeners[i];
if (listenerAtIndex == listener) {
@@ -253,8 +262,7 @@
/// Discards any resources used by the object. After this is called, the
/// object is not in a usable state and should be discarded (calls to
- /// [addListener] and [removeListener] will throw after the object is
- /// disposed).
+ /// [addListener] will throw after the object is disposed).
///
/// This method should only be called by the object's owner.
@mustCallSuper
@@ -264,6 +272,8 @@
_debugDisposed = true;
return true;
}());
+ _listeners = _emptyListeners;
+ _count = 0;
}
/// Call all the registered listeners.
diff --git a/packages/flutter/test/foundation/change_notifier_test.dart b/packages/flutter/test/foundation/change_notifier_test.dart
index 5cb1509..d17d33f 100644
--- a/packages/flutter/test/foundation/change_notifier_test.dart
+++ b/packages/flutter/test/foundation/change_notifier_test.dart
@@ -323,16 +323,13 @@
expect(log, isEmpty);
});
- test('Cannot use a disposed ChangeNotifier', () {
+ test('Cannot use a disposed ChangeNotifier except for remove listener', () {
final TestNotifier source = TestNotifier();
source.dispose();
expect(() {
source.addListener(() {});
}, throwsFlutterError);
expect(() {
- source.removeListener(() {});
- }, throwsFlutterError);
- expect(() {
source.dispose();
}, throwsFlutterError);
expect(() {
@@ -340,6 +337,18 @@
}, throwsFlutterError);
});
+ test('Can remove listener on a disposed ChangeNotifier', () {
+ final TestNotifier source = TestNotifier();
+ FlutterError? error;
+ try {
+ source.dispose();
+ source.removeListener(() {});
+ } on FlutterError catch (e) {
+ error = e;
+ }
+ expect(error, isNull);
+ });
+
test('Value notifier', () {
final ValueNotifier<double> notifier = ValueNotifier<double>(2.0);