Unverified Commit efc07965 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Allow reuse of NavigatorObserver in Navigator.observers (#81601)

parent 04bb954a
...@@ -1113,9 +1113,11 @@ abstract class State<T extends StatefulWidget> with Diagnosticable { ...@@ -1113,9 +1113,11 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
/// The framework calls this method whenever it removes this [State] object /// The framework calls this method whenever it removes this [State] object
/// from the tree. In some cases, the framework will reinsert the [State] /// 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 /// 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 /// [State] object is grafted from one location in the tree to another due to
/// that happens, the framework will ensure that it calls [build] to give the /// the use of a [GlobalKey]). If that happens, the framework will call
/// [State] object a chance to adapt to its new location in the tree. If /// [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 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 /// the animation frame in which the subtree was removed from the tree. For
/// this reason, [State] objects can defer releasing most resources until the /// this reason, [State] objects can defer releasing most resources until the
...@@ -1136,6 +1138,40 @@ abstract class State<T extends StatefulWidget> with Diagnosticable { ...@@ -1136,6 +1138,40 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
@mustCallSuper @mustCallSuper
void deactivate() { } 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. /// Called when this object is removed from the tree permanently.
/// ///
/// The framework calls this method when this [State] object will never /// The framework calls this method when this [State] object will never
...@@ -4804,6 +4840,7 @@ class StatefulElement extends ComponentElement { ...@@ -4804,6 +4840,7 @@ class StatefulElement extends ComponentElement {
@override @override
void activate() { void activate() {
super.activate(); super.activate();
state.activate();
// Since the State could have observed the deactivate() and thus disposed of // Since the State could have observed the deactivate() and thus disposed of
// resources allocated in the build method, we have to rebuild the widget // resources allocated in the build method, we have to rebuild the widget
// so that its State can reallocate its resources. // so that its State can reallocate its resources.
......
...@@ -3628,6 +3628,22 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res ...@@ -3628,6 +3628,22 @@ class NavigatorState extends State<Navigator> 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 @override
void dispose() { void dispose() {
assert(!_debugLocked); assert(!_debugLocked);
...@@ -3635,9 +3651,12 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res ...@@ -3635,9 +3651,12 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
_debugLocked = true; _debugLocked = true;
return true; return true;
}()); }());
assert(() {
for (final NavigatorObserver observer in _effectiveObservers)
assert(observer._navigator != this);
return true;
}());
_updateHeroController(null); _updateHeroController(null);
for (final NavigatorObserver observer in _effectiveObservers)
observer._navigator = null;
focusScopeNode.dispose(); focusScopeNode.dispose();
for (final _RouteEntry entry in _history) for (final _RouteEntry entry in _history)
entry.dispose(); entry.dispose();
......
...@@ -1589,6 +1589,40 @@ void main() { ...@@ -1589,6 +1589,40 @@ void main() {
expect(() => element.state, throwsA(isA<TypeError>())); expect(() => element.state, throwsA(isA<TypeError>()));
expect(() => element.widget, throwsA(isA<TypeError>())); expect(() => element.widget, throwsA(isA<TypeError>()));
}, skip: kIsWeb); }, skip: kIsWeb);
testWidgets('Deactivate and activate are called correctly', (WidgetTester tester) async {
final List<String> states = <String>[];
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<void> pumpWidget(Widget widget) {
states.clear();
return tester.pumpWidget(widget);
}
await pumpWidget(build());
expect(states, <String>['initState', 'build']);
await pumpWidget(Container(child: build()));
expect(states, <String>['deactivate', 'initState', 'build', 'dispose']);
await pumpWidget(Container());
expect(states, <String>['deactivate', 'dispose']);
final GlobalKey key = GlobalKey();
await pumpWidget(build(key));
expect(states, <String>['initState', 'build']);
await pumpWidget(Container(child: build(key)));
expect(states, <String>['deactivate', 'activate', 'didUpdateWidget', 'build']);
await pumpWidget(Container());
expect(states, <String>['deactivate', 'dispose']);
});
} }
class _WidgetWithNoVisitChildren extends StatelessWidget { class _WidgetWithNoVisitChildren extends StatelessWidget {
...@@ -1827,6 +1861,7 @@ class StatefulWidgetSpy extends StatefulWidget { ...@@ -1827,6 +1861,7 @@ class StatefulWidgetSpy extends StatefulWidget {
this.onDidChangeDependencies, this.onDidChangeDependencies,
this.onDispose, this.onDispose,
this.onDeactivate, this.onDeactivate,
this.onActivate,
this.onDidUpdateWidget, this.onDidUpdateWidget,
}) : super(key: key); }) : super(key: key);
...@@ -1835,6 +1870,7 @@ class StatefulWidgetSpy extends StatefulWidget { ...@@ -1835,6 +1870,7 @@ class StatefulWidgetSpy extends StatefulWidget {
final void Function(BuildContext)? onDidChangeDependencies; final void Function(BuildContext)? onDidChangeDependencies;
final void Function(BuildContext)? onDispose; final void Function(BuildContext)? onDispose;
final void Function(BuildContext)? onDeactivate; final void Function(BuildContext)? onDeactivate;
final void Function(BuildContext)? onActivate;
final void Function(BuildContext)? onDidUpdateWidget; final void Function(BuildContext)? onDidUpdateWidget;
@override @override
...@@ -1854,6 +1890,12 @@ class _StatefulWidgetSpyState extends State<StatefulWidgetSpy> { ...@@ -1854,6 +1890,12 @@ class _StatefulWidgetSpyState extends State<StatefulWidgetSpy> {
widget.onDeactivate?.call(context); widget.onDeactivate?.call(context);
} }
@override
void activate() {
super.activate();
widget.onActivate?.call(context);
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
......
...@@ -3560,6 +3560,42 @@ void main() { ...@@ -3560,6 +3560,42 @@ void main() {
expect(observations[7].previous, isNull); 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: <NavigatorObserver>[observer],
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<void>(
settings: settings,
pageBuilder: (BuildContext _, Animation<double> __, Animation<double> ___) {
return Container();
},
);
},
),
);
}
// Test without reinsertion
await tester.pumpWidget(build());
await tester.pumpWidget(Container(child: build()));
expect(observer.navigator, tester.state<NavigatorState>(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<NavigatorState>(find.byType(Navigator)));
});
} }
typedef AnnouncementCallBack = void Function(Route<dynamic>?); typedef AnnouncementCallBack = void Function(Route<dynamic>?);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment