feat: removeRoute now call didComplete (#157725)

**(edited 4 december) PR EDITED TO FIT LATEST COMMENTS**

Developper may want to remove a route in the background while an other
one is displayed.
To do so, developer can use removeRoute().

RemoveRoute() didn't resolve futures created by Navigator.push and so
code which await for Navigator.push will never end.

By calling complete() inside removeRoute, futures are well resolved. In
addition that also allow to pass some result through removeRoute.

Ex: 

```dart
Navigator.of(context).removeRoute(route, "test");
Navigator.of(context).removeRoute(route, object);
```

This PR aim to fix this issue :
https://github.com/flutter/flutter/issues/157505

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Enguerrand ARMINJON 2025-02-12 23:36:49 +01:00 committed by GitHub
parent 16b7ab69dc
commit 585e20a15f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 585 additions and 114 deletions

View File

@ -104,7 +104,7 @@ transforms:
- title: "Remove 'vsync'" - title: "Remove 'vsync'"
date: 2023-01-30 date: 2023-01-30
element: element:
uris: ['widgets.dart', 'material.dart', 'cupertino.dart'] uris: [ 'widgets.dart', 'material.dart', 'cupertino.dart' ]
constructor: '' constructor: ''
inClass: 'AnimatedSize' inClass: 'AnimatedSize'
changes: changes:
@ -115,7 +115,7 @@ transforms:
- title: "Migrate to 'boldTextOf'" - title: "Migrate to 'boldTextOf'"
date: 2022-10-28 date: 2022-10-28
element: element:
uris: ['widgets.dart', 'material.dart', 'cupertino.dart'] uris: [ 'widgets.dart', 'material.dart', 'cupertino.dart' ]
method: 'boldTextOverride' method: 'boldTextOverride'
inClass: 'MediaQuery' inClass: 'MediaQuery'
changes: changes:
@ -855,8 +855,8 @@ transforms:
field: 'clipToSize' field: 'clipToSize'
inClass: 'ListWheelViewport' inClass: 'ListWheelViewport'
changes: changes:
- kind: 'rename' - kind: 'rename'
newName: 'clipBehavior' newName: 'clipBehavior'
# Changes made in https://docs.flutter.dev/release/breaking-changes/clip-behavior # Changes made in https://docs.flutter.dev/release/breaking-changes/clip-behavior
- title: "Migrate to 'clipBehavior'" - title: "Migrate to 'clipBehavior'"
@ -866,28 +866,28 @@ transforms:
constructor: '' constructor: ''
inClass: 'ListWheelViewport' inClass: 'ListWheelViewport'
oneOf: oneOf:
- if: "clipToSize == 'true'" - if: "clipToSize == 'true'"
changes: changes:
- kind: 'addParameter' - kind: 'addParameter'
index: 13 index: 13
name: 'clipBehavior' name: 'clipBehavior'
style: optional_named style: optional_named
argumentValue: argumentValue:
expression: 'Clip.hardEdge' expression: 'Clip.hardEdge'
requiredIf: "clipToSize == 'true'" requiredIf: "clipToSize == 'true'"
- kind: 'removeParameter' - kind: 'removeParameter'
name: 'clipToSize' name: 'clipToSize'
- if: "clipToSize == 'false'" - if: "clipToSize == 'false'"
changes: changes:
- kind: 'addParameter' - kind: 'addParameter'
index: 13 index: 13
name: 'clipBehavior' name: 'clipBehavior'
style: optional_named style: optional_named
argumentValue: argumentValue:
expression: 'Clip.none' expression: 'Clip.none'
requiredIf: "clipToSize == 'false'" requiredIf: "clipToSize == 'false'"
- kind: 'removeParameter' - kind: 'removeParameter'
name: 'clipToSize' name: 'clipToSize'
variables: variables:
clipToSize: clipToSize:
kind: 'fragment' kind: 'fragment'
@ -938,7 +938,18 @@ transforms:
method: 'buildViewportChrome' method: 'buildViewportChrome'
inClass: 'ScrollBehavior' inClass: 'ScrollBehavior'
changes: changes:
- kind: 'rename' - kind: 'rename'
newName: 'buildOverscrollIndicator' newName: 'buildOverscrollIndicator'
# Changes made in https://github.com/flutter/flutter/pull/157725
- title: "Migrate to 'markForComplete'"
date: 2025-02-10
element:
uris: [ 'widgets.dart' ]
method: 'markForRemove'
inClass: 'RouteTransitionRecord'
changes:
- kind: 'rename'
newName: 'markForComplete'
# Before adding a new fix: read instructions at the top of this file. # Before adding a new fix: read instructions at the top of this file.

View File

@ -952,7 +952,12 @@ abstract class RouteTransitionRecord {
/// During [TransitionDelegate.resolve], this can be called on an exiting /// During [TransitionDelegate.resolve], this can be called on an exiting
/// route to indicate that the route should be removed from the [Navigator] /// route to indicate that the route should be removed from the [Navigator]
/// without completing and without an animated transition. /// without completing and without an animated transition.
void markForRemove(); @Deprecated(
'Call markForComplete instead. '
'This will let route associated future to complete when route is removed. '
'This feature was deprecated after v3.27.0-1.0.pre.',
)
void markForRemove() => markForComplete();
} }
/// The delegate that decides how pages added and removed from [Navigator.pages] /// The delegate that decides how pages added and removed from [Navigator.pages]
@ -987,11 +992,11 @@ abstract class RouteTransitionRecord {
/// } /// }
/// for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) { /// for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
/// if (exitingPageRoute.isWaitingForExitingDecision) { /// if (exitingPageRoute.isWaitingForExitingDecision) {
/// exitingPageRoute.markForRemove(); /// exitingPageRoute.markForComplete();
/// final List<RouteTransitionRecord>? pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; /// final List<RouteTransitionRecord>? pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
/// if (pagelessRoutes != null) { /// if (pagelessRoutes != null) {
/// for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { /// for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
/// pagelessRoute.markForRemove(); /// pagelessRoute.markForComplete();
/// } /// }
/// } /// }
/// } /// }
@ -1112,8 +1117,7 @@ abstract class TransitionDelegate<T> {
/// route requires explicit decision on how it should transition off the /// route requires explicit decision on how it should transition off the
/// Navigator. To make a decision for a removed route, call /// Navigator. To make a decision for a removed route, call
/// [RouteTransitionRecord.markForPop], /// [RouteTransitionRecord.markForPop],
/// [RouteTransitionRecord.markForComplete] or /// [RouteTransitionRecord.markForComplete]. It is possible that decisions are
/// [RouteTransitionRecord.markForRemove]. It is possible that decisions are
/// not required for routes in the `locationToExitingPageRoute`. This can /// not required for routes in the `locationToExitingPageRoute`. This can
/// happen if the routes have already been popped in earlier page updates and /// happen if the routes have already been popped in earlier page updates and
/// are still waiting for popping animations to finish. In such case, those /// are still waiting for popping animations to finish. In such case, those
@ -1159,8 +1163,6 @@ abstract class TransitionDelegate<T> {
/// without an animated transition. /// without an animated transition.
/// * [RouteTransitionRecord.markForPop], which makes route exit the screen /// * [RouteTransitionRecord.markForPop], which makes route exit the screen
/// with an animated transition. /// with an animated transition.
/// * [RouteTransitionRecord.markForRemove], which does not complete the
/// route and makes it exit the screen without an animated transition.
/// * [RouteTransitionRecord.markForComplete], which completes the route and /// * [RouteTransitionRecord.markForComplete], which completes the route and
/// makes it exit the screen without an animated transition. /// makes it exit the screen without an animated transition.
/// * [DefaultTransitionDelegate.resolve], which implements the default way /// * [DefaultTransitionDelegate.resolve], which implements the default way
@ -2187,9 +2189,9 @@ class Navigator extends StatefulWidget {
/// and [Route.didChangeNext]). If the [Navigator] has any /// and [Route.didChangeNext]). If the [Navigator] has any
/// [Navigator.observers], they will be notified as well (see /// [Navigator.observers], they will be notified as well (see
/// [NavigatorObserver.didPush] and [NavigatorObserver.didRemove]). The /// [NavigatorObserver.didPush] and [NavigatorObserver.didRemove]). The
/// removed routes are disposed, without being notified, once the new route /// removed routes are disposed, once the new route has finished animating,
/// has finished animating. The futures that had been returned from pushing /// and the futures that had been returned from pushing those routes
/// those routes will not complete. /// will complete.
/// ///
/// Ongoing gestures within the current route are canceled when a new route is /// Ongoing gestures within the current route are canceled when a new route is
/// pushed. /// pushed.
@ -2463,7 +2465,7 @@ class Navigator extends StatefulWidget {
/// they will be notified as well (see [NavigatorObserver.didPush] and /// they will be notified as well (see [NavigatorObserver.didPush] and
/// [NavigatorObserver.didRemove]). The removed routes are disposed of and /// [NavigatorObserver.didRemove]). The removed routes are disposed of and
/// notified, once the new route has finished animating. The futures that had /// notified, once the new route has finished animating. The futures that had
/// been returned from pushing those routes will not complete. /// been returned from pushing those routes will complete.
/// ///
/// Ongoing gestures within the current route are canceled when a new route is /// Ongoing gestures within the current route are canceled when a new route is
/// pushed. /// pushed.
@ -2544,16 +2546,14 @@ class Navigator extends StatefulWidget {
/// _does_ animate the new route, and delays removing the old route until the /// _does_ animate the new route, and delays removing the old route until the
/// new route has finished animating. /// new route has finished animating.
/// ///
/// The removed route is removed without being completed, so this method does /// The removed route is removed and completed with a `null` value.
/// not take a return value argument.
/// ///
/// The new route, the route below the new route (if any), and the route above /// The new route, the route below the new route (if any), and the route above
/// the new route, are all notified (see [Route.didReplace], /// the new route, are all notified (see [Route.didReplace],
/// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator] /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator]
/// has any [Navigator.observers], they will be notified as well (see /// has any [Navigator.observers], they will be notified as well (see
/// [NavigatorObserver.didReplace]). The removed route is disposed without /// [NavigatorObserver.didReplace]). The removed route is disposed with its
/// being notified. The future that had been returned from pushing that routes /// future completed.
/// will not complete.
/// ///
/// This can be useful in combination with [removeRouteBelow] when building a /// This can be useful in combination with [removeRouteBelow] when building a
/// non-linear user experience. /// non-linear user experience.
@ -2615,16 +2615,14 @@ class Navigator extends StatefulWidget {
/// _does_ animate the new route, and delays removing the old route until the /// _does_ animate the new route, and delays removing the old route until the
/// new route has finished animating. /// new route has finished animating.
/// ///
/// The removed route is removed without being completed, so this method does /// The removed route is removed and completed with a `null` value.
/// not take a return value argument.
/// ///
/// The new route, the route below the new route (if any), and the route above /// The new route, the route below the new route (if any), and the route above
/// the new route, are all notified (see [Route.didReplace], /// the new route, are all notified (see [Route.didReplace],
/// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator] /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator]
/// has any [Navigator.observers], they will be notified as well (see /// has any [Navigator.observers], they will be notified as well (see
/// [NavigatorObserver.didReplace]). The removed route is disposed without /// [NavigatorObserver.didReplace]). The removed route is disposed with its
/// being notified. The future that had been returned from pushing that routes /// future completed.
/// will not complete.
/// ///
/// The `T` type argument is the type of the return value of the new route. /// The `T` type argument is the type of the return value of the new route.
/// {@endtemplate} /// {@endtemplate}
@ -2815,27 +2813,35 @@ class Navigator extends StatefulWidget {
/// the given context, and [Route.dispose] it. /// the given context, and [Route.dispose] it.
/// ///
/// {@template flutter.widgets.navigator.removeRoute} /// {@template flutter.widgets.navigator.removeRoute}
/// The removed route is removed without being completed, so this method does /// No animations are run as a result of this method call.
/// not take a return value argument. No animations are run as a result of
/// this method call.
/// ///
/// The routes below and above the removed route are notified (see /// The routes below and above the removed route are notified (see
/// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator] /// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator]
/// has any [Navigator.observers], they will be notified as well (see /// has any [Navigator.observers], they will be notified as well (see
/// [NavigatorObserver.didRemove]). The removed route is disposed without /// [NavigatorObserver.didRemove]). The removed route is disposed with its
/// being notified. The future that had been returned from pushing that routes /// future completed.
/// will not complete.
/// ///
/// The given `route` must be in the history; this method will throw an /// The given `route` must be in the history; this method will throw an
/// exception if it is not. /// exception if it is not.
/// ///
/// If non-null, `result` will be used as the result of the route that is
/// removed; the future that had been returned from pushing the removed route
/// will complete with `result`. If provided, must match the type argument of
/// the class of the popped route (`T`).
///
/// The `T` type argument is the type of the return value of the popped route.
///
/// The type of `result`, if provided, must match the type argument of the
/// class of the removed route (`T`).
///
/// Ongoing gestures within the current route are canceled. /// Ongoing gestures within the current route are canceled.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// This method is used, for example, to instantly dismiss dropdown menus that /// This method is used, for example, to instantly dismiss dropdown menus that
/// are up when the screen's orientation changes. /// are up when the screen's orientation changes.
static void removeRoute(BuildContext context, Route<dynamic> route) { @optionalTypeArgs
return Navigator.of(context).removeRoute(route); static void removeRoute<T extends Object?>(BuildContext context, Route<T> route, [T? result]) {
return Navigator.of(context).removeRoute<T>(route, result);
} }
/// Immediately remove a route from the navigator that most tightly encloses /// Immediately remove a route from the navigator that most tightly encloses
@ -2843,24 +2849,36 @@ class Navigator extends StatefulWidget {
/// one below the given `anchorRoute`. /// one below the given `anchorRoute`.
/// ///
/// {@template flutter.widgets.navigator.removeRouteBelow} /// {@template flutter.widgets.navigator.removeRouteBelow}
/// The removed route is removed without being completed, so this method does /// No animations are run as a result of this method call.
/// not take a return value argument. No animations are run as a result of
/// this method call.
/// ///
/// The routes below and above the removed route are notified (see /// The routes below and above the removed route are notified (see
/// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator] /// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator]
/// has any [Navigator.observers], they will be notified as well (see /// has any [Navigator.observers], they will be notified as well (see
/// [NavigatorObserver.didRemove]). The removed route is disposed without /// [NavigatorObserver.didRemove]). The removed route is disposed with its
/// being notified. The future that had been returned from pushing that routes /// future completed.
/// will not complete.
/// ///
/// The given `anchorRoute` must be in the history and must have a route below /// The given `anchorRoute` must be in the history and must have a route below
/// it; this method will throw an exception if it is not or does not. /// it; this method will throw an exception if it is not or does not.
/// ///
/// If non-null, `result` will be used as the result of the route that is
/// removed; the future that had been returned from pushing the removed route
/// will complete with `result`. If provided, must match the type argument of
/// the class of the popped route (`T`).
///
/// The `T` type argument is the type of the return value of the popped route.
///
/// The type of `result`, if provided, must match the type argument of the
/// class of the removed route (`T`).
///
/// Ongoing gestures within the current route are canceled. /// Ongoing gestures within the current route are canceled.
/// {@endtemplate} /// {@endtemplate}
static void removeRouteBelow(BuildContext context, Route<dynamic> anchorRoute) { @optionalTypeArgs
return Navigator.of(context).removeRouteBelow(anchorRoute); static void removeRouteBelow<T extends Object?>(
BuildContext context,
Route<T> anchorRoute, [
T? result,
]) {
return Navigator.of(context).removeRouteBelow<T>(anchorRoute, result);
} }
/// The state from the closest instance of this class that encloses the given /// The state from the closest instance of this class that encloses the given
@ -3348,21 +3366,6 @@ class _RouteEntry extends RouteTransitionRecord {
bool _reportRemovalToObserver = true; bool _reportRemovalToObserver = true;
// Route is removed without being completed.
void remove({bool isReplaced = false}) {
assert(
!pageBased || isWaitingForExitingDecision,
'A page-based route cannot be completed using imperative api, provide a '
'new list without the corresponding Page to Navigator.pages instead. ',
);
if (currentState.index >= _RouteLifecycle.remove.index) {
return;
}
assert(isPresent);
_reportRemovalToObserver = !isReplaced;
currentState = _RouteLifecycle.remove;
}
// Route completes with `result` and is removed. // Route completes with `result` and is removed.
void complete<T>(T result, {bool isReplaced = false}) { void complete<T>(T result, {bool isReplaced = false}) {
assert( assert(
@ -3561,18 +3564,6 @@ class _RouteEntry extends RouteTransitionRecord {
_isWaitingForExitingDecision = false; _isWaitingForExitingDecision = false;
} }
@override
void markForRemove() {
assert(
!isWaitingForEnteringDecision && isWaitingForExitingDecision && isPresent,
'This route cannot be marked for remove. Either a decision has already '
'been made or it does not require an explicit decision on how to transition '
'out.',
);
remove();
_isWaitingForExitingDecision = false;
}
bool get restorationEnabled => route.restorationScopeId.value != null; bool get restorationEnabled => route.restorationScopeId.value != null;
set restorationEnabled(bool value) { set restorationEnabled(bool value) {
assert(!value || restorationId != null); assert(!value || restorationId != null);
@ -5315,7 +5306,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
_history.add(entry); _history.add(entry);
while (index >= 0 && !predicate(_history[index].route)) { while (index >= 0 && !predicate(_history[index].route)) {
if (_history[index].isPresent) { if (_history[index].isPresent) {
_history[index].remove(); _history[index].complete(null);
} }
index -= 1; index -= 1;
} }
@ -5400,7 +5391,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
); );
final bool wasCurrent = oldRoute.isCurrent; final bool wasCurrent = oldRoute.isCurrent;
_history.insert(index + 1, entry); _history.insert(index + 1, entry);
_history[index].remove(isReplaced: true); _history[index].complete(null, isReplaced: true);
_flushHistoryUpdates(); _flushHistoryUpdates();
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
@ -5490,7 +5481,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
} }
assert(index >= 0, 'There are no routes below the specified anchorRoute.'); assert(index >= 0, 'There are no routes below the specified anchorRoute.');
_history.insert(index + 1, entry); _history.insert(index + 1, entry);
_history[index].remove(isReplaced: true); _history[index].complete(null, isReplaced: true);
_flushHistoryUpdates(); _flushHistoryUpdates();
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
@ -5654,7 +5645,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
/// Immediately remove `route` from the navigator, and [Route.dispose] it. /// Immediately remove `route` from the navigator, and [Route.dispose] it.
/// ///
/// {@macro flutter.widgets.navigator.removeRoute} /// {@macro flutter.widgets.navigator.removeRoute}
void removeRoute(Route<dynamic> route) { @optionalTypeArgs
void removeRoute<T extends Object?>(Route<T> route, [T? result]) {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { assert(() {
_debugLocked = true; _debugLocked = true;
@ -5663,7 +5655,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
assert(route._navigator == this); assert(route._navigator == this);
final bool wasCurrent = route.isCurrent; final bool wasCurrent = route.isCurrent;
final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route)); final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route));
entry.remove(); entry.complete(result);
_flushHistoryUpdates(rearrangeOverlay: false); _flushHistoryUpdates(rearrangeOverlay: false);
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
@ -5678,7 +5670,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
/// route to be removed is the one below the given `anchorRoute`. /// route to be removed is the one below the given `anchorRoute`.
/// ///
/// {@macro flutter.widgets.navigator.removeRouteBelow} /// {@macro flutter.widgets.navigator.removeRouteBelow}
void removeRouteBelow(Route<dynamic> anchorRoute) { @optionalTypeArgs
void removeRouteBelow<T extends Object?>(Route<T> anchorRoute, [T? result]) {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { assert(() {
_debugLocked = true; _debugLocked = true;
@ -5699,7 +5692,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
index -= 1; index -= 1;
} }
assert(index >= 0, 'There are no routes below the specified anchorRoute.'); assert(index >= 0, 'There are no routes below the specified anchorRoute.');
_history[index].remove(); _history[index].complete(result);
_flushHistoryUpdates(rearrangeOverlay: false); _flushHistoryUpdates(rearrangeOverlay: false);
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;

View File

@ -16,6 +16,14 @@ import 'navigator_utils.dart';
import 'observer_tester.dart'; import 'observer_tester.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
@pragma('vm:entry-point')
Route<void> _routeBuilder(BuildContext context, Object? arguments) {
return MaterialPageRoute<void>(
settings: const RouteSettings(name: 'route'),
builder: (BuildContext context) => Container(),
);
}
class FirstWidget extends StatelessWidget { class FirstWidget extends StatelessWidget {
const FirstWidget({super.key}); const FirstWidget({super.key});
@override @override
@ -1459,12 +1467,409 @@ void main() {
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle(); await tester.pumpAndSettle();
pageValue.then((String? value) {
assert(false);
});
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator)); final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.removeRoute(routes['/A']!); // stack becomes /, pageValue will not complete navigator.removeRoute(
routes['/A']!,
'B',
); // stack becomes /, pageValue will complete and return 'B'
expect(await pageValue, 'B');
});
testWidgets('remove route below an other one whose value is awaited', (
WidgetTester tester,
) async {
late Future<String?> pageValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageValue = Navigator.pushNamed(context, '/A');
},
),
'/A':
(BuildContext context) => OnTapPage(
id: '/A',
onTap: () {
Navigator.pushNamed(context, '/B');
},
),
'/B':
(BuildContext context) => OnTapPage(
id: 'B',
onTap: () {
Navigator.pop(context, 'B');
},
),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
await tester.tap(find.text('/A')); // pushNamed('/B'), stack becomes /, /A, /B
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.removeRouteBelow(
routes['/B']!,
'A',
); // stack becomes /, /B, pageValue will complete and return 'A'
expect(await pageValue, 'A');
});
testWidgets('replace route by an other whose value is awaited', (WidgetTester tester) async {
late Future<String?> pageValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageValue = Navigator.pushNamed(context, '/A');
},
),
'/A': (BuildContext context) => const OnTapPage(id: '/A'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
final MaterialPageRoute<void> routeB = MaterialPageRoute<void>(
builder: (BuildContext context) => const OnTapPage(id: '/B'),
);
navigator.replace(
oldRoute: routes['/A']!,
newRoute: routeB,
); // stack becomes /, /B, pageValue will complete and return 'A'
expect(await pageValue, null);
});
testWidgets('replace route by an other whose value is awaited', (WidgetTester tester) async {
late Future<String?> pageValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageValue = Navigator.pushNamed(context, '/A');
},
),
'/A': (BuildContext context) => const OnTapPage(id: '/A'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
final MaterialPageRoute<void> routeB = MaterialPageRoute<void>(
builder: (BuildContext context) => const OnTapPage(id: '/B'),
);
navigator.replace(
oldRoute: routes['/A']!,
newRoute: routeB,
); // stack becomes /, /B, pageValue will complete and return 'A'
expect(await pageValue, null);
});
testWidgets('restorable replace route by an other whose value is awaited', (
WidgetTester tester,
) async {
late Future<String?> pageAValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageAValue = Navigator.pushNamed(context, '/A');
},
),
'/A': (BuildContext context) => const OnTapPage(id: '/A'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.restorableReplace(
oldRoute: routes['/A']!,
newRouteBuilder: _routeBuilder,
); // stack becomes /, /route, pageValue will complete and return 'A'
expect(await pageAValue, null);
});
testWidgets('push named route and remove until where routes values are awaited', (
WidgetTester tester,
) async {
late Future<String?> pageAValue;
late Future<String?> pageBValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageAValue = Navigator.pushNamed(context, '/A');
},
),
'/A':
(BuildContext context) => OnTapPage(
id: '/A',
onTap: () {
pageBValue = Navigator.pushNamed(context, '/B');
},
),
'/B': (BuildContext context) => const OnTapPage(id: '/B'),
'/C': (BuildContext context) => const OnTapPage(id: '/C'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
await tester.tap(find.text('/A')); // pushNamed('/B'), stack becomes /, /A, /B
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.pushNamedAndRemoveUntil(
'/C',
ModalRoute.withName('/'),
); // stack becomes /, /C, pageAValue & pageBValue will complete and return null
expect(await pageAValue, null);
expect(await pageBValue, null);
});
testWidgets('push route and remove until where routes values are awaited', (
WidgetTester tester,
) async {
late Future<String?> pageAValue;
late Future<String?> pageBValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageAValue = Navigator.pushNamed(context, '/A');
},
),
'/A':
(BuildContext context) => OnTapPage(
id: '/A',
onTap: () {
pageBValue = Navigator.pushNamed(context, '/B');
},
),
'/B': (BuildContext context) => const OnTapPage(id: '/B'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
await tester.tap(find.text('/A')); // pushNamed('/B'), stack becomes /, /A, /B
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
final MaterialPageRoute<void> routeC = MaterialPageRoute<void>(
builder: (BuildContext context) => const OnTapPage(id: '/C'),
);
navigator.pushAndRemoveUntil(
routeC,
ModalRoute.withName('/'),
); // stack becomes /, /C, pageAValue & pageBValue will complete and return null
expect(await pageAValue, null);
expect(await pageBValue, null);
});
testWidgets('restorable push named and remove until where routes values are awaited', (
WidgetTester tester,
) async {
late Future<String?> pageAValue;
late Future<String?> pageBValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageAValue = Navigator.pushNamed(context, '/A');
},
),
'/A':
(BuildContext context) => OnTapPage(
id: '/A',
onTap: () {
pageBValue = Navigator.pushNamed(context, '/B');
},
),
'/B': (BuildContext context) => const OnTapPage(id: '/B'),
'/C': (BuildContext context) => const OnTapPage(id: '/C'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
await tester.tap(find.text('/A')); // pushNamed('/B'), stack becomes /, /A, /B
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.restorablePushNamedAndRemoveUntil(
'/C',
ModalRoute.withName('/'),
); // stack becomes /, /C, pageAValue & pageBValue will complete and return null
expect(await pageAValue, null);
expect(await pageBValue, null);
});
testWidgets('restorable push and remove until where routes values are awaited', (
WidgetTester tester,
) async {
late Future<String?> pageAValue;
late Future<String?> pageBValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/':
(BuildContext context) => OnTapPage(
id: '/',
onTap: () {
pageAValue = Navigator.pushNamed(context, '/A');
},
),
'/A':
(BuildContext context) => OnTapPage(
id: '/A',
onTap: () {
pageBValue = Navigator.pushNamed(context, '/B');
},
),
'/B': (BuildContext context) => const OnTapPage(id: '/B'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name!] = PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> _) {
return pageBuilders[settings.name!]!(context);
},
);
return routes[settings.name];
},
),
);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
await tester.tap(find.text('/A')); // pushNamed('/B'), stack becomes /, /A, /B
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.restorablePushAndRemoveUntil(
_routeBuilder,
ModalRoute.withName('/'),
); // stack becomes /, /route, pageAValue & pageBValue will complete and return null
expect(await pageAValue, null);
expect(await pageBValue, null);
}); });
testWidgets('replacing route can be observed', (WidgetTester tester) async { testWidgets('replacing route can be observed', (WidgetTester tester) async {
@ -4027,8 +4432,8 @@ void main() {
transitionDelegate: transitionDelegate, transitionDelegate: transitionDelegate,
), ),
); );
// The pageless route of initial page route should be removed without complete. // The pageless route of initial page route should be removed and completed.
expect(initialPageless1Completed, false); expect(initialPageless1Completed, true);
expect(secondPageless1Completed, false); expect(secondPageless1Completed, false);
expect(secondPageless2Completed, false); expect(secondPageless2Completed, false);
expect(thirdPageless1Completed, false); expect(thirdPageless1Completed, false);
@ -4044,9 +4449,9 @@ void main() {
), ),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(initialPageless1Completed, false); expect(initialPageless1Completed, true);
expect(secondPageless1Completed, false); expect(secondPageless1Completed, true);
expect(secondPageless2Completed, false); expect(secondPageless2Completed, true);
expect(thirdPageless1Completed, false); expect(thirdPageless1Completed, false);
myPages = <TestPage>[const TestPage(key: ValueKey<String>('4'), name: 'forth')]; myPages = <TestPage>[const TestPage(key: ValueKey<String>('4'), name: 'forth')];
@ -4060,10 +4465,10 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
expect(initialPageless1Completed, false); expect(initialPageless1Completed, true);
expect(secondPageless1Completed, false); expect(secondPageless1Completed, true);
expect(secondPageless2Completed, false); expect(secondPageless2Completed, true);
expect(thirdPageless1Completed, false); expect(thirdPageless1Completed, true);
expect(find.text('forth'), findsOneWidget); expect(find.text('forth'), findsOneWidget);
}); });
@ -5859,12 +6264,12 @@ class AlwaysRemoveTransitionDelegate extends TransitionDelegate<void> {
final RouteTransitionRecord exitingPageRoute = locationToExitingPageRoute[location]!; final RouteTransitionRecord exitingPageRoute = locationToExitingPageRoute[location]!;
if (exitingPageRoute.isWaitingForExitingDecision) { if (exitingPageRoute.isWaitingForExitingDecision) {
final bool hasPagelessRoute = pageRouteToPagelessRoutes.containsKey(exitingPageRoute); final bool hasPagelessRoute = pageRouteToPagelessRoutes.containsKey(exitingPageRoute);
exitingPageRoute.markForRemove(); exitingPageRoute.markForComplete();
if (hasPagelessRoute) { if (hasPagelessRoute) {
final List<RouteTransitionRecord> pagelessRoutes = final List<RouteTransitionRecord> pagelessRoutes =
pageRouteToPagelessRoutes[exitingPageRoute]!; pageRouteToPagelessRoutes[exitingPageRoute]!;
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove(); pagelessRoute.markForComplete();
} }
} }
} }

View File

@ -4,6 +4,32 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class _TestRouteTransitionRecord extends RouteTransitionRecord {
@override
bool get isWaitingForEnteringDecision => throw UnimplementedError();
@override
bool get isWaitingForExitingDecision => throw UnimplementedError();
@override
void markForAdd() {}
@override
void markForComplete([dynamic result]) {}
@override
void markForPop([dynamic result]) {}
@override
void markForPush() {}
@override
void markForRemove() {}
@override
Route<dynamic> get route => throw UnimplementedError();
}
void main() { void main() {
// Generic reference variables. // Generic reference variables.
BuildContext context; BuildContext context;
@ -189,4 +215,9 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/139260 // Changes made in https://github.com/flutter/flutter/pull/139260
final NavigatorState state = Navigator.of(context); final NavigatorState state = Navigator.of(context);
state.focusScopeNode; state.focusScopeNode;
// Changes made in https://github.com/flutter/flutter/pull/157725
final _TestRouteTransitionRecord testRouteTransitionRecord =
_TestRouteTransitionRecord();
testRouteTransitionRecord.markForComplete();
} }

View File

@ -4,6 +4,32 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class _TestRouteTransitionRecord extends RouteTransitionRecord {
@override
bool get isWaitingForEnteringDecision => throw UnimplementedError();
@override
bool get isWaitingForExitingDecision => throw UnimplementedError();
@override
void markForAdd() {}
@override
void markForComplete([dynamic result]) {}
@override
void markForPop([dynamic result]) {}
@override
void markForPush() {}
@override
void markForRemove() {}
@override
Route<dynamic> get route => throw UnimplementedError();
}
void main() { void main() {
// Generic reference variables. // Generic reference variables.
BuildContext context; BuildContext context;
@ -189,4 +215,9 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/139260 // Changes made in https://github.com/flutter/flutter/pull/139260
final NavigatorState state = Navigator.of(context); final NavigatorState state = Navigator.of(context);
state.focusNode.enclosingScope!; state.focusNode.enclosingScope!;
// Changes made in https://github.com/flutter/flutter/pull/157725
final _TestRouteTransitionRecord testRouteTransitionRecord =
_TestRouteTransitionRecord();
testRouteTransitionRecord.markForComplete();
} }