diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 88934b5fdc..518229ba5f 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -1113,9 +1113,11 @@ abstract class State with Diagnosticable { /// The framework calls this method whenever it removes this [State] object /// from the tree. In some cases, the framework will reinsert the [State] /// object into another part of the tree (e.g., if the subtree containing this - /// [State] object is grafted from one location in the tree to another). If - /// that happens, the framework will ensure that it calls [build] to give the - /// [State] object a chance to adapt to its new location in the tree. If + /// [State] object is grafted from one location in the tree to another due to + /// the use of a [GlobalKey]). If that happens, the framework will call + /// [activate] to give the [State] object a chance to reacquire any resources + /// that it released in [deactivate]. It will then also call [build] to give + /// the [State] object a chance to adapt to its new location in the tree. If /// the framework does reinsert this subtree, it will do so before the end of /// the animation frame in which the subtree was removed from the tree. For /// this reason, [State] objects can defer releasing most resources until the @@ -1136,6 +1138,40 @@ abstract class State with Diagnosticable { @mustCallSuper void deactivate() { } + /// Called when this object is reinserted into the tree after having been + /// removed via [deactivate]. + /// + /// In most cases, after a [State] object has been deactivated, it is _not_ + /// reinserted into the tree, and its [dispose] method will be called to + /// signal that it is ready to be garbage collected. + /// + /// In some cases, however, after a [State] object has been deactivated, the + /// framework will reinsert it into another part of the tree (e.g., if the + /// subtree containing this [State] object is grafted from one location in + /// the tree to another due to the use of a [GlobalKey]). If that happens, + /// the framework will call [activate] to give the [State] object a chance to + /// reacquire any resources that it released in [deactivate]. It will then + /// also call [build] to give the object a chance to adapt to its new + /// location in the tree. If the framework does reinsert this subtree, it + /// will do so before the end of the animation frame in which the subtree was + /// removed from the tree. For this reason, [State] objects can defer + /// releasing most resources until the framework calls their [dispose] method. + /// + /// The framework does not call this method the first time a [State] object + /// is inserted into the tree. Instead, the framework calls [initState] in + /// that situation. + /// + /// Implementations of this method should start with a call to the inherited + /// method, as in `super.activate()`. + /// + /// See also: + /// + /// * [Element.activate], the corresponding method when an element + /// transitions from the "inactive" to the "active" lifecycle state. + @protected + @mustCallSuper + void activate() { } + /// Called when this object is removed from the tree permanently. /// /// The framework calls this method when this [State] object will never @@ -4804,6 +4840,7 @@ class StatefulElement extends ComponentElement { @override void activate() { super.activate(); + state.activate(); // Since the State could have observed the deactivate() and thus disposed of // resources allocated in the build method, we have to rebuild the widget // so that its State can reallocate its resources. diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 681e0c8b71..0d9a6fac0b 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -3628,6 +3628,22 @@ class NavigatorState extends State with TickerProviderStateMixin, Res }()); } + @override + void deactivate() { + for (final NavigatorObserver observer in _effectiveObservers) + observer._navigator = null; + super.deactivate(); + } + + @override + void activate() { + super.activate(); + for (final NavigatorObserver observer in _effectiveObservers) { + assert(observer.navigator == null); + observer._navigator = this; + } + } + @override void dispose() { assert(!_debugLocked); @@ -3635,9 +3651,12 @@ class NavigatorState extends State with TickerProviderStateMixin, Res _debugLocked = true; return true; }()); + assert(() { + for (final NavigatorObserver observer in _effectiveObservers) + assert(observer._navigator != this); + return true; + }()); _updateHeroController(null); - for (final NavigatorObserver observer in _effectiveObservers) - observer._navigator = null; focusScopeNode.dispose(); for (final _RouteEntry entry in _history) entry.dispose(); diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index 5877251b7f..739f9cad5d 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -1589,6 +1589,40 @@ void main() { expect(() => element.state, throwsA(isA())); expect(() => element.widget, throwsA(isA())); }, skip: kIsWeb); + + testWidgets('Deactivate and activate are called correctly', (WidgetTester tester) async { + final List states = []; + Widget build([Key? key]) { + return StatefulWidgetSpy( + key: key, + onInitState: (BuildContext context) { states.add('initState'); }, + onDidUpdateWidget: (BuildContext context) { states.add('didUpdateWidget'); }, + onDeactivate: (BuildContext context) { states.add('deactivate'); }, + onActivate: (BuildContext context) { states.add('activate'); }, + onBuild: (BuildContext context) { states.add('build'); }, + onDispose: (BuildContext context) { states.add('dispose'); }, + ); + } + Future pumpWidget(Widget widget) { + states.clear(); + return tester.pumpWidget(widget); + } + + await pumpWidget(build()); + expect(states, ['initState', 'build']); + await pumpWidget(Container(child: build())); + expect(states, ['deactivate', 'initState', 'build', 'dispose']); + await pumpWidget(Container()); + expect(states, ['deactivate', 'dispose']); + + final GlobalKey key = GlobalKey(); + await pumpWidget(build(key)); + expect(states, ['initState', 'build']); + await pumpWidget(Container(child: build(key))); + expect(states, ['deactivate', 'activate', 'didUpdateWidget', 'build']); + await pumpWidget(Container()); + expect(states, ['deactivate', 'dispose']); + }); } class _WidgetWithNoVisitChildren extends StatelessWidget { @@ -1827,6 +1861,7 @@ class StatefulWidgetSpy extends StatefulWidget { this.onDidChangeDependencies, this.onDispose, this.onDeactivate, + this.onActivate, this.onDidUpdateWidget, }) : super(key: key); @@ -1835,6 +1870,7 @@ class StatefulWidgetSpy extends StatefulWidget { final void Function(BuildContext)? onDidChangeDependencies; final void Function(BuildContext)? onDispose; final void Function(BuildContext)? onDeactivate; + final void Function(BuildContext)? onActivate; final void Function(BuildContext)? onDidUpdateWidget; @override @@ -1854,6 +1890,12 @@ class _StatefulWidgetSpyState extends State { widget.onDeactivate?.call(context); } + @override + void activate() { + super.activate(); + widget.onActivate?.call(context); + } + @override void dispose() { super.dispose(); diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index b8f323bdce..5175db27b0 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -3560,6 +3560,42 @@ void main() { expect(observations[7].previous, isNull); }); }); + + testWidgets('Can reuse NavigatorObserver in rebuilt tree', (WidgetTester tester) async { + final NavigatorObserver observer = NavigatorObserver(); + Widget build([Key? key]) { + return Directionality( + textDirection: TextDirection.ltr, + child: Navigator( + key: key, + observers: [observer], + onGenerateRoute: (RouteSettings settings) { + return PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext _, Animation __, Animation ___) { + return Container(); + }, + ); + }, + ), + ); + } + + // Test without reinsertion + await tester.pumpWidget(build()); + await tester.pumpWidget(Container(child: build())); + expect(observer.navigator, tester.state(find.byType(Navigator))); + + // Clear the tree + await tester.pumpWidget(Container()); + expect(observer.navigator, isNull); + + // Test with reinsertion + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(build(key)); + await tester.pumpWidget(Container(child: build(key))); + expect(observer.navigator, tester.state(find.byType(Navigator))); + }); } typedef AnnouncementCallBack = void Function(Route?);