diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index c552046d4b6..b1154beb7ed 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.1.0 + +- Adds route redirect to ShellRoutes + ## 14.0.2 - Fixes unwanted logs when `hierarchicalLoggingEnabled` was set to `true`. diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 09764747aae..bef882e1460 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -394,14 +394,13 @@ class RouteConfiguration { return prevMatchList; } - final List routeMatches = []; + final List routeMatches = []; prevMatchList.visitRouteMatches((RouteMatchBase match) { - if (match is RouteMatch) { + if (match.route.redirect != null) { routeMatches.add(match); } return true; }); - final FutureOr routeLevelRedirectResult = _getRouteLevelRedirect(context, prevMatchList, routeMatches, 0); @@ -434,25 +433,22 @@ class RouteConfiguration { FutureOr _getRouteLevelRedirect( BuildContext context, RouteMatchList matchList, - List routeMatches, + List routeMatches, int currentCheckIndex, ) { if (currentCheckIndex >= routeMatches.length) { return null; } - final RouteMatch match = routeMatches[currentCheckIndex]; + final RouteMatchBase match = routeMatches[currentCheckIndex]; FutureOr processRouteRedirect(String? newLocation) => newLocation ?? _getRouteLevelRedirect( context, matchList, routeMatches, currentCheckIndex + 1); - final GoRoute route = match.route; - FutureOr routeRedirectResult; - if (route.redirect != null) { - routeRedirectResult = route.redirect!( - context, - match.buildState(this, matchList), - ); - } + final RouteBase route = match.route; + final FutureOr routeRedirectResult = route.redirect!.call( + context, + match.buildState(this, matchList), + ); if (routeRedirectResult is String?) { return processRouteRedirect(routeRedirectResult); } diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 69dd904f087..b93520b95aa 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -152,10 +152,67 @@ typedef ExitCallback = FutureOr Function( @immutable abstract class RouteBase with Diagnosticable { const RouteBase._({ + this.redirect, required this.routes, required this.parentNavigatorKey, }); + /// An optional redirect function for this route. + /// + /// In the case that you like to make a redirection decision for a specific + /// route (or sub-route), consider doing so by passing a redirect function to + /// the GoRoute constructor. + /// + /// For example: + /// ``` + /// final GoRouter _router = GoRouter( + /// routes: [ + /// GoRoute( + /// path: '/', + /// redirect: (_) => '/family/${Families.data[0].id}', + /// ), + /// GoRoute( + /// path: '/family/:fid', + /// pageBuilder: (BuildContext context, GoRouterState state) => ..., + /// ), + /// ], + /// ); + /// ``` + /// + /// If there are multiple redirects in the matched routes, the parent route's + /// redirect takes priority over sub-route's. + /// + /// For example: + /// ``` + /// final GoRouter _router = GoRouter( + /// routes: [ + /// GoRoute( + /// path: '/', + /// redirect: (_) => '/page1', // this takes priority over the sub-route. + /// routes: [ + /// GoRoute( + /// path: 'child', + /// redirect: (_) => '/page2', + /// ), + /// ], + /// ), + /// ], + /// ); + /// ``` + /// + /// The `context.go('/child')` will be redirected to `/page1` instead of + /// `/page2`. + /// + /// Redirect can also be used for conditionally preventing users from visiting + /// routes, also known as route guards. One canonical example is user + /// authentication. See [Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart) + /// for a complete runnable example. + /// + /// If [BuildContext.dependOnInheritedWidgetOfExactType] is used during the + /// redirection (which is how `of` method is usually implemented), a + /// re-evaluation will be triggered if the [InheritedWidget] changes. + final GoRouterRedirect? redirect; + /// The list of child routes associated with this route. final List routes; @@ -209,7 +266,7 @@ class GoRoute extends RouteBase { this.builder, this.pageBuilder, super.parentNavigatorKey, - this.redirect, + super.redirect, this.onExit, super.routes = const [], }) : assert(path.isNotEmpty, 'GoRoute path cannot be empty'), @@ -325,62 +382,6 @@ class GoRoute extends RouteBase { /// final GoRouterWidgetBuilder? builder; - /// An optional redirect function for this route. - /// - /// In the case that you like to make a redirection decision for a specific - /// route (or sub-route), consider doing so by passing a redirect function to - /// the GoRoute constructor. - /// - /// For example: - /// ``` - /// final GoRouter _router = GoRouter( - /// routes: [ - /// GoRoute( - /// path: '/', - /// redirect: (_) => '/family/${Families.data[0].id}', - /// ), - /// GoRoute( - /// path: '/family/:fid', - /// pageBuilder: (BuildContext context, GoRouterState state) => ..., - /// ), - /// ], - /// ); - /// ``` - /// - /// If there are multiple redirects in the matched routes, the parent route's - /// redirect takes priority over sub-route's. - /// - /// For example: - /// ``` - /// final GoRouter _router = GoRouter( - /// routes: [ - /// GoRoute( - /// path: '/', - /// redirect: (_) => '/page1', // this takes priority over the sub-route. - /// routes: [ - /// GoRoute( - /// path: 'child', - /// redirect: (_) => '/page2', - /// ), - /// ], - /// ), - /// ], - /// ); - /// ``` - /// - /// The `context.go('/child')` will be redirected to `/page1` instead of - /// `/page2`. - /// - /// Redirect can also be used for conditionally preventing users from visiting - /// routes, also known as route guards. One canonical example is user - /// authentication. See [Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart) - /// for a complete runnable example. - /// - /// If [BuildContext.dependOnInheritedWidgetOfExactType] is used during the - /// redirection (which is how `of` method is usually implemented), a - /// re-evaluation will be triggered if the [InheritedWidget] changes. - final GoRouterRedirect? redirect; - /// Called when this route is removed from GoRouter's route history. /// /// Some example this callback may be called: @@ -458,9 +459,11 @@ class GoRoute extends RouteBase { /// as [ShellRoute] and [StatefulShellRoute]. abstract class ShellRouteBase extends RouteBase { /// Constructs a [ShellRouteBase]. - const ShellRouteBase._( - {required super.routes, required super.parentNavigatorKey}) - : super._(); + const ShellRouteBase._({ + super.redirect, + required super.routes, + required super.parentNavigatorKey, + }) : super._(); static void _debugCheckSubRouteParentNavigatorKeys( List subRoutes, GlobalKey navigatorKey) { @@ -623,6 +626,7 @@ class ShellRouteContext { class ShellRoute extends ShellRouteBase { /// Constructs a [ShellRoute]. ShellRoute({ + super.redirect, this.builder, this.pageBuilder, this.observers, @@ -783,6 +787,7 @@ class StatefulShellRoute extends ShellRouteBase { /// [navigatorContainerBuilder]. StatefulShellRoute({ required this.branches, + super.redirect, this.builder, this.pageBuilder, required this.navigatorContainerBuilder, @@ -809,12 +814,14 @@ class StatefulShellRoute extends ShellRouteBase { /// for a complete runnable example using StatefulShellRoute.indexedStack. StatefulShellRoute.indexedStack({ required List branches, + GoRouterRedirect? redirect, StatefulShellRouteBuilder? builder, GlobalKey? parentNavigatorKey, StatefulShellRoutePageBuilder? pageBuilder, String? restorationScopeId, }) : this( branches: branches, + redirect: redirect, builder: builder, pageBuilder: pageBuilder, parentNavigatorKey: parentNavigatorKey, diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 664fa43d782..f04e5433ae3 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -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: 14.0.2 +version: 14.1.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 diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 42bbec0cc4f..639db182f51 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2462,6 +2462,94 @@ void main() { expect( router.routerDelegate.currentConfiguration.uri.toString(), '/other'); }); + + testWidgets('redirect when go to a shell route', + (WidgetTester tester) async { + final List routes = [ + ShellRoute( + redirect: (BuildContext context, GoRouterState state) => '/dummy', + builder: (BuildContext context, GoRouterState state, Widget child) => + Scaffold(appBar: AppBar(), body: child), + routes: [ + GoRoute( + path: '/other', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + GoRoute( + path: '/other2', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + ], + ), + GoRoute( + path: '/dummy', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + + for (final String shellRoute in ['/other', '/other2']) { + router.go(shellRoute); + await tester.pump(); + expect( + router.routerDelegate.currentConfiguration.uri.toString(), + '/dummy', + ); + } + }); + + testWidgets('redirect when go to a stateful shell route', + (WidgetTester tester) async { + final List routes = [ + StatefulShellRoute.indexedStack( + redirect: (BuildContext context, GoRouterState state) => '/dummy', + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + return navigationShell; + }, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/other', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/other2', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + ], + ), + ], + ), + GoRoute( + path: '/dummy', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + + for (final String shellRoute in ['/other', '/other2']) { + router.go(shellRoute); + await tester.pump(); + expect( + router.routerDelegate.currentConfiguration.uri.toString(), + '/dummy', + ); + } + }); }); group('initial location', () {