Skip to content

Commit

Permalink
Merge pull request #681 from slovnicki/feature/beam-page-path
Browse files Browse the repository at this point in the history
Feature: add `String? path` property and `previousPagePathPop` behavior to `BeamPage`
  • Loading branch information
slovnicki authored Oct 8, 2024
2 parents d01c4e6 + a4e4ac1 commit c021ea4
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 41 deletions.
1 change: 1 addition & 0 deletions package/lib/src/beam_location.dart
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ class RoutesBeamLocation extends BeamLocation<BeamState> {
} else {
return BeamPage(
key: ValueKey(filteredRoutes[route]),
path: filteredRoutes[route],
child: routeElement,
);
}
Expand Down
56 changes: 53 additions & 3 deletions package/lib/src/beam_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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;

Expand Down
137 changes: 99 additions & 38 deletions package/test/beam_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,61 @@ class TestLocation extends BeamLocation<BeamState> {
TestLocation([RouteInformation? routeInformation]) : super(routeInformation);

@override
List<String> get pathPatterns => ['/books/:bookId/details/buy'];
List<String> get pathPatterns => [
'/books/:bookId/details/buy',
'/books/:bookId/author/:authorId',
];

@override
List<BeamPage> buildPages(BuildContext context, BeamState state) => [
List<BeamPage> 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() {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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', () {
Expand Down

0 comments on commit c021ea4

Please sign in to comment.