Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[go_router] Path based branch routing for StatefulShellRoute - deprecating goBranch #7622

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,10 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
initialLocation: '/a',
routes: <RouteBase>[
StatefulShellRoute(
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
// This nested StatefulShellRoute demonstrates the use of a
// custom container for the branch Navigators. In this implementation,
// no customization is done in the builder function (navigationShell
// itself is simply used as the Widget for the route). Instead, the
// navigatorContainerBuilder function below is provided to
// customize the container for the branch Navigators.
return navigationShell;
},
// This nested StatefulShellRoute demonstrates the use of a custom
// container for the branch Navigators, using the
// `navigatorContainerBuilder` parameter. When doing so, the `builder`
// should not be provided, and `pageBuilder` is optional.
navigatorContainerBuilder: (BuildContext context,
StatefulNavigationShell navigationShell, List<Widget> children) {
// Returning a customized container for the branch
Expand Down Expand Up @@ -208,17 +202,15 @@ class ScaffoldWithNavBar extends StatelessWidget {
/// Navigate to the current location of the branch at the provided index when
/// tapping an item in the BottomNavigationBar.
void _onTap(BuildContext context, int index) {
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
// Navigator for the branch is restored.
navigationShell.goBranch(
index,
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the initialLocation parameter of goBranch.
initialLocation: index == navigationShell.currentIndex,
);
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the '/initial' shell route redirect.
if (index == navigationShell.currentIndex) {
GoRouter.of(context).go('/shell/$index/initial');
} else {
GoRouter.of(context).go('/shell/$index');
}
}
}

Expand Down
93 changes: 60 additions & 33 deletions packages/go_router/example/lib/stateful_shell_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,21 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
initialLocation: '/a',
routes: <RouteBase>[
// #docregion configuration-builder
StatefulShellRoute.indexedStack(
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
StatefulShellRoute.indexedStackContainer(
path: '/shell',
builder: (BuildContext context, ShellRouteState state, Widget child) {
// Return the widget that implements the custom shell (in this case
// using a BottomNavigationBar). The StatefulNavigationShell is passed
// to be able access the state of the shell and to navigate to other
// branches in a stateful way.
return ScaffoldWithNavBar(navigationShell: navigationShell);
return ScaffoldWithNavBar(shellState: state, child: child);
},
// #enddocregion configuration-builder
// #docregion configuration-branches
branches: <StatefulShellBranch>[
// The route branch for the first tab of the bottom navigation bar.
StatefulShellBranch(
name: 'branchA',
navigatorKey: _sectionANavigatorKey,
routes: <RouteBase>[
GoRoute(
Expand All @@ -68,6 +69,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget {

// The route branch for the second tab of the bottom navigation bar.
StatefulShellBranch(
name: 'branchB',
// It's not necessary to provide a navigatorKey if it isn't also
// needed elsewhere. If not provided, a default key will be used.
routes: <RouteBase>[
Expand Down Expand Up @@ -97,6 +99,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget {

// The route branch for the third tab of the bottom navigation bar.
StatefulShellBranch(
name: 'branchC',
routes: <RouteBase>[
GoRoute(
// The screen to display as the root in the third tab of the
Expand Down Expand Up @@ -142,55 +145,79 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
class ScaffoldWithNavBar extends StatelessWidget {
/// Constructs an [ScaffoldWithNavBar].
const ScaffoldWithNavBar({
required this.navigationShell,
required this.shellState,
required this.child,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));

/// The state of the shell route.
final ShellRouteState shellState;

/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
final Widget child;

StatefulShellRoute get _shellRoute =>
shellState.shellRoute as StatefulShellRoute;

// #docregion configuration-custom-shell
@override
Widget build(BuildContext context) {
return Scaffold(
// The StatefulNavigationShell from the associated StatefulShellRoute is
// directly passed as the body of the Scaffold.
body: navigationShell,
body: child,
bottomNavigationBar: BottomNavigationBar(
// Here, the items of BottomNavigationBar are hard coded. In a real
// world scenario, the items would most likely be generated from the
// branches of the shell route, which can be fetched using
// `navigationShell.route.branches`.
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'),
],
currentIndex: navigationShell.currentIndex,
// Navigate to the current location of the branch at the provided index
// when tapping an item in the BottomNavigationBar.
onTap: (int index) => navigationShell.goBranch(index),
),
// Here, the items of BottomNavigationBar are hard coded. In a real
// world scenario, the items would most likely be generated from the
// branches of the shell route, which can be fetched using
// `navigationShell.route.branches`.
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'),
],
currentIndex: shellState.navigatorIndex,
// Navigate to the current location of the branch at the provided index
// when tapping an item in the BottomNavigationBar.
onTap: (int index) {
// It's possible to simply do this to navigate between branches:
// return GoRouter.of(context).go('/shell/$index');
// But since names are configured for the branches, it is also
// possible to restore a branch by name, like this:
return switch (index) {
1 => GoRouter.of(context).go('/shell/branchB'),
2 => GoRouter.of(context).go('/shell/branchC'),
_ => GoRouter.of(context).go('/shell/branchA'),
};
}),
);
}
// #enddocregion configuration-custom-shell

/// NOTE: For a slightly more sophisticated branch switching, change the onTap
/// handler on the BottomNavigationBar above to the following:
/// `onTap: (int index) => _onTap(context, index),`
/// `onTap: _onTap,`
// ignore: unused_element
void _onTap(BuildContext context, int index) {
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
// Navigator for the branch is restored.
navigationShell.goBranch(
index,
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the initialLocation parameter of goBranch.
initialLocation: index == navigationShell.currentIndex,
);
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the '/initial' shell route redirect.
if (index == shellState.navigatorIndex) {
final String initialLocation = shellState.initialLocation(index)!;
GoRouter.of(context).go(initialLocation);
// It is also possible to navigate to the initial location of the branch
// like this:
//GoRouter.of(context).go('/shell/$index/initial');
} else {
return switch (index) {
1 => GoRouter.of(context).go('/shell/branchB'),
2 => GoRouter.of(context).go('/shell/branchC'),
_ => GoRouter.of(context).go('/shell/branchA'),
};
// It is also possible to navigate to the branch by index like this:
//GoRouter.of(context).go('/shell/$index');
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/lib/go_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export 'src/misc/extensions.dart';
export 'src/misc/inherited_router.dart';
export 'src/pages/custom_transition_page.dart';
export 'src/parser.dart';
export 'src/route.dart';
export 'src/route.dart' hide StatefulShellRestoreStateRedirect;
export 'src/route_data.dart' hide NoOpPage;
export 'src/router.dart';
export 'src/state.dart' hide GoRouterStateRegistry, GoRouterStateRegistryScope;
49 changes: 22 additions & 27 deletions packages/go_router/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -263,34 +263,29 @@ class _CustomNavigatorState extends State<_CustomNavigator> {
BuildContext context,
ShellRouteMatch match,
) {
final GoRouterState state =
match.buildState(widget.configuration, widget.matchList);
final ShellRouteState state = match.buildState(
widget.configuration, widget.matchList) as ShellRouteState;
final GlobalKey<NavigatorState> navigatorKey = match.navigatorKey;
final ShellRouteContext shellRouteContext = ShellRouteContext(
route: match.route,
routerState: state,
navigatorKey: navigatorKey,
routeMatchList: widget.matchList,
navigatorBuilder:
(List<NavigatorObserver>? observers, String? restorationScopeId) {
return _CustomNavigator(
// The state needs to persist across rebuild.
key: GlobalObjectKey(navigatorKey.hashCode),
navigatorRestorationId: restorationScopeId,
navigatorKey: navigatorKey,
matches: match.matches,
matchList: widget.matchList,
configuration: widget.configuration,
observers: observers ?? const <NavigatorObserver>[],
onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch,
// This is used to recursively build pages under this shell route.
errorBuilder: widget.errorBuilder,
errorPageBuilder: widget.errorPageBuilder,
);
},
);
Widget navigatorBuilder(
List<NavigatorObserver>? observers, String? restorationScopeId) {
return _CustomNavigator(
// The state needs to persist across rebuild.
key: GlobalObjectKey(navigatorKey.hashCode),
navigatorRestorationId: restorationScopeId,
navigatorKey: navigatorKey,
matches: match.matches,
matchList: widget.matchList,
configuration: widget.configuration,
observers: observers ?? const <NavigatorObserver>[],
onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch,
// This is used to recursively build pages under this shell route.
errorBuilder: widget.errorBuilder,
errorPageBuilder: widget.errorPageBuilder,
);
}

final Page<Object?>? page =
match.route.buildPage(context, state, shellRouteContext);
match.route.buildPage(context, state, navigatorBuilder);
if (page != null && page is! NoOpPage) {
return page;
}
Expand All @@ -301,7 +296,7 @@ class _CustomNavigatorState extends State<_CustomNavigator> {
state,
Builder(
builder: (BuildContext context) {
return match.route.buildWidget(context, state, shellRouteContext)!;
return match.route.buildWidget(context, state, navigatorBuilder)!;
},
),
);
Expand Down
72 changes: 70 additions & 2 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -393,14 +393,54 @@ class RouteConfiguration {
}

final List<RouteMatchBase> routeMatches = <RouteMatchBase>[];
ShellRouteMatch? statefulShellMatch;
prevMatchList.visitRouteMatches((RouteMatchBase match) {
statefulShellMatch =
match is ShellRouteMatch && match.route is StatefulShellRoute
? match
: statefulShellMatch;
if (match.route.redirect != null) {
routeMatches.add(match);
}
return true;
});
final FutureOr<String?> routeLevelRedirectResult =
_getRouteLevelRedirect(context, prevMatchList, routeMatches, 0);

final FutureOr<String?> routeLevelRedirectResult;
final RouteMatchBase? firstMatch = routeMatches.firstOrNull;
if (firstMatch != null &&
statefulShellMatch != null &&
statefulShellMatch!.matches.contains(firstMatch) &&
firstMatch.route is StatefulShellRestoreStateRedirect) {
int? findBranchIndex(StatefulShellRoute route, String? branchRef) {
final int index = route.branches.indexWhere(
(StatefulShellBranch branch) => branch.name == branchRef);
return index >= 0 ? index : int.tryParse(branchRef ?? '');
}

final StatefulShellRoute shellRoute =
statefulShellMatch!.route as StatefulShellRoute;
final StatefulShellRestoreStateRedirect shellRedirect =
firstMatch.route as StatefulShellRestoreStateRedirect;
final String? branchRef = shellRedirect
.branchReferenceFromPathParameters(prevMatchList.pathParameters);
final int? branchIndex = findBranchIndex(shellRoute, branchRef);
final RouteMatchList? restoredMatchList =
shellRedirect.restoreState(branchIndex);
if (restoredMatchList != null && restoredMatchList.isNotEmpty) {
return redirect(
context,
restoredMatchList,
redirectHistory: redirectHistory,
);
}

// If there is no restored state, redirect to the initial location
routeLevelRedirectResult = initialLocationForShellNavigator(
shellRoute, branchIndex ?? shellRoute.initialBranchIndex);
} else {
routeLevelRedirectResult =
_getRouteLevelRedirect(context, prevMatchList, routeMatches, 0);
}

if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
Expand Down Expand Up @@ -510,6 +550,34 @@ class RouteConfiguration {
String? locationForRoute(RouteBase route) =>
fullPathForRoute(route, '', _routingConfig.value.routes);

/// Gets the effective initial location for the nested navigator at the
/// provided index in the provided [ShellRouteBase].
///
/// For a [StatefulShellRoute], the effective initial location is either the
/// [StatefulShellBranch.initialLocation], if specified, or the location of the
/// [StatefulShellBranch.defaultRoute].
String? initialLocationForShellNavigator(ShellRouteBase route, int index) {
if (route is StatefulShellRoute) {
final StatefulShellBranch branch = route.branches[index];
final String? initialLocation = branch.initialLocation;
if (initialLocation != null) {
return initialLocation;
} else {
/// Recursively traverses the routes of the provided StatefulShellRoute to
/// find the first GoRoute, from which a full path will be derived.
final GoRoute route = branch.defaultRoute!;
final List<String> parameters = <String>[];
patternToRegExp(route.path, parameters);
assert(parameters.isEmpty);
return locationForRoute(route)!;
// TODO(tolo): Unsure what the original purpose of below was, but it seems odd to involve the current routerState.pathParameters when determining the initial location.
// return patternToPath(
// fullPath, shellRouteContext.routerState.pathParameters);
}
}
return null;
}

@override
String toString() {
return 'RouterConfiguration: ${_routingConfig.value.routes}';
Expand Down
6 changes: 6 additions & 0 deletions packages/go_router/lib/src/information_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ enum NavigatingType {

/// Restore the current match list with
/// [RouteInformationState.baseRouteMatchList].
// TODO(tolo): Remove this in next major release.
@Deprecated(
'Configure path on StatefulShellRoute and use GoRouter.go instead.')
restore,
}

Expand Down Expand Up @@ -173,6 +176,9 @@ class GoRouteInformationProvider extends RouteInformationProvider
}

/// Restores the current route matches with the `matchList`.
// TODO(tolo): Remove this in next major release.
@Deprecated(
'Configure path on StatefulShellRoute and use GoRouter.go instead.')
void restore(String location, {required RouteMatchList matchList}) {
_setValue(
matchList.uri.toString(),
Expand Down
Loading