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);