From 0e521942a3d7e317ded68b91f6c00efcc8810724 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:00:55 -0700 Subject: [PATCH] Allows page removal that contains Localhistoryentry (#134757) fixes https://github.com/flutter/flutter/issues/97836 fixes https://github.com/flutter/flutter/issues/134752 fixes https://github.com/flutter/flutter/issues/118645 --- .../flutter/lib/src/widgets/navigator.dart | 18 ++++ packages/flutter/lib/src/widgets/routes.dart | 8 -- .../flutter/test/widgets/navigator_test.dart | 84 +++++++++++++++++++ 3 files changed, 102 insertions(+), 8 deletions(-) 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 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(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 extends TransitionRoute with LocalHistoryRoute '${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> myPages = const >[ + MaterialPage(child: Text('page1')), + MaterialPage(child: Text('page2')), + ]; + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + expect(find.text('page2'), findsOneWidget); + final ModalRoute route = ModalRoute.of(tester.element(find.text('page2')))!; + bool entryRemoved = false; + route.addLocalHistoryEntry(LocalHistoryEntry(onRemove: () => entryRemoved = true)); + expect(route.willHandlePopInternally, true); + + myPages = const >[ + MaterialPage(child: Text('page1')), + ]; + + await tester.pumpWidget( + MediaQuery( + data: MediaQueryData.fromView(tester.view), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const >[ + 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> myPages = >[ + MaterialPage(child: Text('page1')), + MaterialPage( + 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 >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Navigator( + pages: myPages, + onPopPage: (_, __) => false, + ), + ), + ), + ); + final ModalRoute 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 secondaryAnimationOfRouteOne; late Animation primaryAnimationOfRouteOne;