diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index e2f350d299b0..41b570be858f 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -2927,6 +2927,10 @@ class _RouteEntry extends RouteTransitionRecord { final _RestorationInformation? restorationInformation; final bool pageBased; + /// The limit this route entry will attempt to pop in the case of route being + /// remove as a result of a page update. + static const int kDebugPopAttemptLimit = 100; + static final Route<dynamic> notAnnounced = _NotAnnounced(); _RouteLifecycle currentState; @@ -3268,6 +3272,20 @@ class _RouteEntry extends RouteTransitionRecord { 'This route cannot be marked for pop. Either a decision has already been ' 'made or it does not require an explicit decision on how to transition out.', ); + // Remove state that prevents a pop, e.g. LocalHistoryEntry[s]. + int attempt = 0; + while (route.willHandlePopInternally) { + assert( + () { + attempt += 1; + return attempt < kDebugPopAttemptLimit; + }(), + 'Attempted to pop $route $kDebugPopAttemptLimit times, but still failed', + ); + final bool popResult = route.didPop(result); + assert(!popResult); + + } pop<dynamic>(result); _isWaitingForExitingDecision = false; } diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index b51cb4f61c7c..d779df692d58 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -1832,14 +1832,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ]; } - @override - bool get willHandlePopInternally { - final bool popEntriesCanPop = _popEntries.every((PopEntry popEntry) { - return popEntry.canPopNotifier.value; - }); - return !popEntriesCanPop || super.willHandlePopInternally; - } - @override String toString() => '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)'; } diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 886de4a71269..8044583a7b27 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -2910,6 +2910,90 @@ void main() { ); }); + testWidgets('Can pop route with local history entries using page api', (WidgetTester tester) async { + List<Page<void>> myPages = const <Page<void>>[ + MaterialPage<void>(child: Text('page1')), + MaterialPage<void>(child: Text('page2')), + ]; + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + expect(find.text('page2'), findsOneWidget); + final ModalRoute<void> route = ModalRoute.of(tester.element(find.text('page2')))!; + bool entryRemoved = false; + route.addLocalHistoryEntry(LocalHistoryEntry(onRemove: () => entryRemoved = true)); + expect(route.willHandlePopInternally, true); + + myPages = const <Page<void>>[ + MaterialPage<void>(child: Text('page1')), + ]; + + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + expect(find.text('page1'), findsOneWidget); + expect(entryRemoved, isTrue); + }); + + testWidgets('ModalRoute must comply with willHandlePopInternally when there is a PopScope', (WidgetTester tester) async { + const List<Page<void>> myPages = <Page<void>>[ + MaterialPage<void>(child: Text('page1')), + MaterialPage<void>( + child: PopScope( + canPop: false, + child: Text('page2'), + ), + ), + ]; + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + final ModalRoute<void> route = ModalRoute.of(tester.element(find.text('page2')))!; + // PopScope only prevents user trigger action, e.g. Navigator.maybePop. + // The page can still be popped by the system if it needs to. + expect(route.willHandlePopInternally, false); + expect(route.didPop(null), true); + }); + testWidgets('can push and pop pages using page api', (WidgetTester tester) async { late Animation<double> secondaryAnimationOfRouteOne; late Animation<double> primaryAnimationOfRouteOne;