From a4e4ac1984405ac756fb189d1777eecbcbd170e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Lovni=C4=8Dki?= Date: Thu, 3 Oct 2024 04:10:31 +0200 Subject: [PATCH] {beam_page}: [add] path and previousPagePathPop behavior --- package/lib/src/beam_location.dart | 1 + package/lib/src/beam_page.dart | 56 +++++++++++- package/test/beam_page_test.dart | 137 +++++++++++++++++++++-------- 3 files changed, 153 insertions(+), 41 deletions(-) diff --git a/package/lib/src/beam_location.dart b/package/lib/src/beam_location.dart index 84c8029..48f169a 100644 --- a/package/lib/src/beam_location.dart +++ b/package/lib/src/beam_location.dart @@ -496,6 +496,7 @@ class RoutesBeamLocation extends BeamLocation { } else { return BeamPage( key: ValueKey(filteredRoutes[route]), + path: filteredRoutes[route], child: routeElement, ); } diff --git a/package/lib/src/beam_page.dart b/package/lib/src/beam_page.dart index a15d394..0bb2009 100644 --- a/package/lib/src/beam_page.dart +++ b/package/lib/src/beam_page.dart @@ -43,16 +43,17 @@ class BeamPage extends Page { const BeamPage({ LocalKey? key, String? name, + this.path, required this.child, this.title, - this.onPopPage = pathSegmentPop, + this.onPopPage = _onPopPage, this.popToNamed, this.type = BeamPageType.material, this.routeBuilder, this.fullScreenDialog = false, this.opaque = true, this.keepQueryOnPop = false, - }) : super(key: key, name: name); + }) : super(key: key, name: name ?? path); /// A [BeamPage] to be the default for [BeamerDelegate.notFoundPage]. static const notFound = BeamPage( @@ -61,8 +62,49 @@ class BeamPage extends Page { child: Scaffold(body: Center(child: Text('Not found'))), ); - /// The default pop behavior for [BeamPage]. + /// The pop behavior for [BeamPage] if [popToNamed] is not specified. + static bool _onPopPage( + BuildContext context, + BeamerDelegate delegate, + RouteInformationSerializable state, + BeamPage poppedPage, + ) { + final success = previousPagePathPop(context, delegate, state, poppedPage); + if (success) { + return true; + } + + return pathSegmentPop(context, delegate, state, poppedPage); + } + + /// The default pop behavior for [BeamPage] if [popToNamed] is not specified. /// + /// Calls [BeamerDelegate.popToNamed] with the [path] of previous [BeamPage], + /// the one below this [BeamPage] on the stack. + static bool previousPagePathPop( + BuildContext context, + BeamerDelegate delegate, + RouteInformationSerializable state, + BeamPage poppedPage, + ) { + final currentPages = delegate.currentPages; + if (currentPages.length < 2) { + return false; + } + + final previousPage = currentPages[currentPages.length - 2]; + if (previousPage.path == null) { + return false; + } + + delegate.update( + configuration: RouteInformation( + uri: Uri.parse(previousPage.path!), + ), + ); + return true; + } + /// Pops the last path segment from URI and calls [BeamerDelegate.update]. static bool pathSegmentPop( BuildContext context, @@ -172,6 +214,14 @@ class BeamPage extends Page { return true; } + /// The path of this [BeamPage] and corresponding [Route]. + /// + /// This will be used when popping this [BeamPage] from the stack, + /// as a more natural way to define the pop behavior than [popToNamed]. + /// + /// This will most likely become required in v2. + final String? path; + /// The concrete Widget representing app's screen. final Widget child; diff --git a/package/test/beam_page_test.dart b/package/test/beam_page_test.dart index 8717bdf..ede50a8 100644 --- a/package/test/beam_page_test.dart +++ b/package/test/beam_page_test.dart @@ -6,48 +6,61 @@ class TestLocation extends BeamLocation { TestLocation([RouteInformation? routeInformation]) : super(routeInformation); @override - List get pathPatterns => ['/books/:bookId/details/buy']; + List get pathPatterns => [ + '/books/:bookId/details/buy', + '/books/:bookId/author/:authorId', + ]; @override - List buildPages(BuildContext context, BeamState state) => [ + List buildPages(BuildContext context, BeamState state) { + final bookId = state.pathParameters['bookId']; + final authorId = state.pathParameters['authorId']; + return [ + BeamPage( + key: const ValueKey('home'), + child: Container(), + ), + if (state.pathPatternSegments.contains('books')) BeamPage( - key: const ValueKey('home'), + key: const ValueKey('books'), + onPopPage: (context, delegate, _, page) { + return false; + }, child: Container(), ), - if (state.pathPatternSegments.contains('books')) - BeamPage( - key: const ValueKey('books'), - onPopPage: (context, delegate, _, page) { - return false; - }, - child: Container(), - ), - if (state.pathParameters.containsKey('bookId')) - BeamPage( - key: ValueKey('book-${state.pathParameters['bookId']}'), - popToNamed: '/', - child: Container(), - ), - if (state.pathPatternSegments.contains('details')) - BeamPage( - key: ValueKey('book-${state.pathParameters['bookId']}-details'), - onPopPage: (context, delegate, _, page) { - delegate.currentBeamLocation.update( - (state) => (state as BeamState).copyWith( - pathPatternSegments: ['books'], - pathParameters: {}, - ), - ); - return true; - }, - child: Container(), - ), - if (state.pathPatternSegments.contains('buy')) - BeamPage( - key: ValueKey('book-${state.pathParameters['bookId']}-buy'), - child: Container(), - ), - ]; + if (bookId != null) + BeamPage( + key: ValueKey('book-$bookId'), + path: '/books/$bookId', + popToNamed: '/', + child: Container(), + ), + if (authorId != null) + BeamPage( + key: ValueKey('author-$authorId'), + child: Container(), + ), + if (state.pathPatternSegments.contains('details')) + BeamPage( + key: ValueKey('book-$bookId-details'), + onPopPage: (context, delegate, _, page) { + delegate.currentBeamLocation.update( + (state) => (state as BeamState).copyWith( + pathPatternSegments: ['books'], + pathParameters: {}, + ), + ); + return true; + }, + child: Container(), + ), + if (state.pathPatternSegments.contains('buy')) + BeamPage( + key: ValueKey('book-$bookId-buy'), + child: Container(), + ), + ]; + } } void main() { @@ -408,7 +421,7 @@ void main() { (delegate.currentBeamLocation.state as BeamState).uri.path, '/test'); }); - testWidgets('pageles', (tester) async { + testWidgets('pageless', (tester) async { final delegate = BeamerDelegate( transitionDelegate: const NoAnimationTransitionDelegate(), locationBuilder: RoutesLocationBuilder( @@ -478,6 +491,54 @@ void main() { await tester.pump(); expect(delegate.configuration.uri.path, '/'); }); + + testWidgets('path of previous page is popped to in BeamLocation', + (tester) async { + await tester.pumpWidget( + MaterialApp.router( + routeInformationParser: BeamerParser(), + routerDelegate: delegate, + ), + ); + delegate.beamToNamed('/books/1/author/2'); + await tester.pump(); + expect(delegate.currentPages.length, 4); + expect(delegate.currentPages.last.key, const ValueKey('author-2')); + + delegate.navigator.pop(); + await tester.pump(); + expect(delegate.currentPages.length, 3); + expect(delegate.currentPages.last.key, const ValueKey('book-1')); + expect(delegate.configuration.uri.path, '/books/1'); + }); + + testWidgets('path of previous page is popped to in RoutesLocationBuilder', + (tester) async { + final delegate = BeamerDelegate( + locationBuilder: RoutesLocationBuilder( + routes: { + '/': (context, state, data) => Container(), + '/books/1': (context, state, data) => Container(), + }, + ), + ); + + await tester.pumpWidget( + MaterialApp.router( + routeInformationParser: BeamerParser(), + routerDelegate: delegate, + ), + ); + delegate.beamToNamed('/books/1'); + await tester.pump(); + expect(delegate.currentPages.length, 2); + expect(delegate.configuration.uri.path, '/books/1'); + + delegate.navigator.pop(); + await tester.pump(); + expect(delegate.currentPages.length, 1); + expect(delegate.configuration.uri.path, '/'); + }); }); group('Transitions', () {