Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/go_router/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ migrate_working_dir/
.dart_tool/
.packages
build/
/coverage/
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 17.4.0

- Adds `maybePop` to `GoRouter`, `GoRouterDelegate`, and `GoRouterHelper`. This method mirrors [Navigator.maybePop] and [BackButton] by returning `false` instead of throwing when there is nothing to pop, and calls [GoRouter.restore] when a pop completes synchronously.

## 17.3.0

- Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.
Expand Down
21 changes: 21 additions & 0 deletions packages/go_router/lib/src/delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList> with ChangeNotifie
Future<bool> popRoute() async {
final Iterable<NavigatorState> states = _findCurrentNavigators();
for (final state in states) {
if (!state.mounted) {
continue;
}
final bool didPop = await state.maybePop(); // Call maybePop() directly
if (didPop) {
return true; // Return true if maybePop handled the pop
Expand Down Expand Up @@ -103,6 +106,24 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList> with ChangeNotifie
states.first.pop(result);
}

/// Calls [NavigatorState.maybePop] on the current navigator stack.
///
/// Returns `true` if a route was popped and `false` otherwise. This method
/// does not throw if there is nothing to pop.
Future<bool> maybePop<T extends Object?>([T? result]) async {
final Iterable<NavigatorState> states = _findCurrentNavigators();
for (final NavigatorState state in states) {
if (!state.mounted) {
continue;
}
final bool didPop = await state.maybePop<T>(result);
if (didPop) {
return true;
}
}
return false;
}
Comment thread
muhammadkamel marked this conversation as resolved.

/// Get a prioritized list of NavigatorStates,
/// which either can pop or are exit routes.
///
Expand Down
10 changes: 10 additions & 0 deletions packages/go_router/lib/src/misc/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ extension GoRouterHelper on BuildContext {
/// Returns `true` if there is more than 1 page on the stack.
bool canPop() => GoRouter.of(this).canPop();

/// Pop the top page off the Navigator's page stack if possible.
///
/// Returns `true` if a route was popped and `false` otherwise. This method
/// does not throw if there is nothing to pop.
///
/// See also:
/// * [pop], which throws if there is nothing to pop.
/// * [canPop], which can be used to check whether a pop is possible.
Future<bool> maybePop<T extends Object?>([T? result]) => GoRouter.of(this).maybePop<T>(result);

/// Pop the top page off the Navigator's page stack by calling
/// [Navigator.pop].
void pop<T extends Object?>([T? result]) => GoRouter.of(this).pop(result);
Expand Down
23 changes: 23 additions & 0 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,10 @@ class GoRouter implements RouterConfig<RouteMatchList> {
///
/// Ensure that the `value` of `routeInformationProvider` is synced
/// with `routerDelegate.currentConfiguration`.
///
/// See also:
/// * [maybePop], which returns `false` instead of throwing if there is
/// nothing to pop.
void pop<T extends Object?>([T? result]) {
assert(() {
log('popping ${routerDelegate.currentConfiguration.uri}');
Expand All @@ -566,6 +570,25 @@ class GoRouter implements RouterConfig<RouteMatchList> {
}
}

/// Pop the top-most route off the current screen if possible.
///
/// This method calls [NavigatorState.maybePop] on the underlying navigators,
/// similar to [BackButton]. It returns `true` if a route was popped and
/// `false` otherwise. Unlike [pop], this method does not throw if there is
/// nothing to pop.
///
/// When a pop completes synchronously, this method also calls [restore] to
/// keep the [routeInformationProvider] in sync with
/// [GoRouterDelegate.currentConfiguration].
Future<bool> maybePop<T extends Object?>([T? result]) async {
final RouteMatchList configBeforePop = routerDelegate.currentConfiguration;
final bool didPop = await routerDelegate.maybePop<T>(result);
if (didPop && !identical(routerDelegate.currentConfiguration, configBeforePop)) {
restore(routerDelegate.currentConfiguration);
}
return didPop;
}

/// Refresh the route.
void refresh() {
assert(() {
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 17.3.0
version: 17.4.0
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down
147 changes: 147 additions & 0 deletions packages/go_router/test/delegate_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,111 @@ void main() {
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 0);
expect(goRouter.routerDelegate.canPop(), false);
});

testWidgets('It should return false on screen A and true on screen B after push', (
WidgetTester tester,
) async {
final GoRouter goRouter = GoRouter(
initialLocation: '/a',
routes: <GoRoute>[
GoRoute(path: '/a', builder: (_, _) => const Text('Screen A')),
GoRoute(path: '/b', builder: (_, _) => const Text('Screen B')),
],
);
addTearDown(goRouter.dispose);
await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter));
await tester.pumpAndSettle();

expect(find.text('Screen A'), findsOneWidget);
expect(goRouter.routerDelegate.canPop(), isFalse);

goRouter.push('/b');
await tester.pumpAndSettle();

expect(find.text('Screen B'), findsOneWidget);
expect(goRouter.routerDelegate.canPop(), isTrue);

goRouter.pop();
await tester.pumpAndSettle();

expect(find.text('Screen A'), findsOneWidget);
expect(goRouter.routerDelegate.canPop(), isFalse);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.uri.path, '/a');
});

testWidgets('It should check shell navigator when root navigator cannot pop', (
WidgetTester tester,
) async {
final shellNavigatorKey = GlobalKey<NavigatorState>();
final GoRouter goRouter = GoRouter(
initialLocation: '/a',
routes: <RouteBase>[
ShellRoute(
navigatorKey: shellNavigatorKey,
builder: (_, _, Widget child) => child,
routes: <GoRoute>[
GoRoute(path: '/a', builder: (_, _) => const Text('Screen A')),
GoRoute(path: '/b', builder: (_, _) => const Text('Screen B')),
],
),
],
);
addTearDown(goRouter.dispose);
await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter));
await tester.pumpAndSettle();

expect(goRouter.routerDelegate.canPop(), isFalse);
expect(goRouter.routerDelegate.navigatorKey.currentState?.canPop(), isFalse);

goRouter.push('/b');
await tester.pumpAndSettle();

expect(goRouter.routerDelegate.canPop(), isTrue);
expect(shellNavigatorKey.currentState?.canPop(), isTrue);
expect(goRouter.routerDelegate.navigatorKey.currentState?.canPop(), isFalse);

goRouter.pop();
await tester.pumpAndSettle();

expect(find.text('Screen A'), findsOneWidget);
expect(goRouter.routerDelegate.canPop(), isFalse);
expect(shellNavigatorKey.currentState?.canPop(), isFalse);
});
});

group('willHandlePopInternally', () {
testWidgets('does not remove route when page handles pop internally', (
WidgetTester tester,
) async {
final GoRouter goRouter = GoRouter(
initialLocation: '/',
routes: <GoRoute>[
GoRoute(
path: '/',
builder: (_, _) => const Text('Home'),
routes: <GoRoute>[
GoRoute(
path: 'internal',
pageBuilder: (_, _) => const _InternalPopPage(),
),
],
),
],
);
addTearDown(goRouter.dispose);
await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter));

goRouter.push('/internal');
await tester.pumpAndSettle();
expect(find.text('Internal'), findsOneWidget);

goRouter.pop();
await tester.pumpAndSettle();

expect(find.text('Internal'), findsOneWidget);
expect(find.text('Home'), findsNothing);
});
});

group('pushReplacement', () {
Expand Down Expand Up @@ -752,3 +857,45 @@ class _DummyStatefulWidgetState extends State<DummyStatefulWidget> {
@override
Widget build(BuildContext context) => Container();
}

class _InternalPopPage extends Page<void> {
const _InternalPopPage();

@override
Route<void> createRoute(BuildContext context) => _InternalPopRoute(this);
}

class _InternalPopRoute extends PageRoute<void> {
_InternalPopRoute(_InternalPopPage page) : super(settings: page);

@override
Color? get barrierColor => null;

@override
String? get barrierLabel => null;

@override
bool get opaque => true;

@override
bool get maintainState => true;

@override
Duration get transitionDuration => Duration.zero;

@override
bool get willHandlePopInternally => true;

@override
// ignore: must_call_super
bool didPop(void result) => false;

@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const Text('Internal');
}
}
Loading