Skip to content

[go_router] Improvement of StatefulShellRoute API - deprecating goBranch #7622

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

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _tabANavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'tabANav');
final GlobalKey<NavigatorState> _tabBNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'tabBNav');
final GlobalKey _shellKey = GlobalKey(debugLabel: 'shellKey');

// This example demonstrates how to setup nested navigation using a
// BottomNavigationBar, where each bar item uses its own persistent navigator,
Expand All @@ -33,25 +36,22 @@ 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;
},
navigatorContainerBuilder: (BuildContext context,
StatefulNavigationShell navigationShell, List<Widget> children) {
// To ensure state is maintained even when route configurations is
// reloaded, provide a global key to the ShellRoute.
key: _shellKey,
name: 'rootShell',
// 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, ShellRouteState state,
List<Widget> children) {
// Returning a customized container for the branch
// Navigators (i.e. the `List<Widget> children` argument).
//
// See ScaffoldWithNavBar for more details on how the children
// are managed (using AnimatedBranchContainer).
return ScaffoldWithNavBar(
navigationShell: navigationShell, children: children);
return ScaffoldWithNavBar(shellState: state, children: children);
},
branches: <StatefulShellBranch>[
// The route branch for the first tab of the bottom navigation bar.
Expand Down Expand Up @@ -80,29 +80,24 @@ class NestedTabNavigationExampleApp extends StatelessWidget {

// The route branch for the third tab of the bottom navigation bar.
StatefulShellBranch(
navigatorKey: _tabBNavigatorKey,
// StatefulShellBranch will automatically use the first descendant
// GoRoute as the initial location of the branch. If another route
// is desired, specify the location of it using the defaultLocation
// parameter.
// defaultLocation: '/c2',
routes: <RouteBase>[
StatefulShellRoute(
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
// Just like with the top level StatefulShellRoute, no
// customization is done in the builder function.
return navigationShell;
},
name: 'nestedShell',
navigatorContainerBuilder: (BuildContext context,
StatefulNavigationShell navigationShell,
List<Widget> children) {
ShellRouteState state, List<Widget> children) {
// Returning a customized container for the branch
// Navigators (i.e. the `List<Widget> children` argument).
//
// See TabbedRootScreen for more details on how the children
// are managed (in a TabBarView).
return TabbedRootScreen(
navigationShell: navigationShell, children: children);
shellState: state, children: children);
},
// This bottom tab uses a nested shell, wrapping sub routes in a
// top TabBar.
Expand Down Expand Up @@ -171,13 +166,13 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
class ScaffoldWithNavBar extends StatelessWidget {
/// Constructs an [ScaffoldWithNavBar].
const ScaffoldWithNavBar({
required this.navigationShell,
required this.shellState,
required this.children,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));

/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
final ShellRouteState shellState;

/// The children (branch Navigators) to display in a custom container
/// ([AnimatedBranchContainer]).
Expand All @@ -187,7 +182,7 @@ class ScaffoldWithNavBar extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBranchContainer(
currentIndex: navigationShell.currentIndex,
currentIndex: shellState.navigatorIndex,
children: children,
),
bottomNavigationBar: BottomNavigationBar(
Expand All @@ -199,7 +194,7 @@ class ScaffoldWithNavBar extends StatelessWidget {
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
],
currentIndex: navigationShell.currentIndex,
currentIndex: shellState.navigatorIndex,
onTap: (int index) => _onTap(context, index),
),
);
Expand All @@ -208,17 +203,13 @@ 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.
if (index == shellState.navigatorIndex) {
shellState.resetNavigatorLocation(index);
}
context.restore(shellState.navigatorLocation(index));
}
}

Expand Down Expand Up @@ -377,10 +368,10 @@ class DetailsScreenState extends State<DetailsScreen> {
class TabbedRootScreen extends StatefulWidget {
/// Constructs a TabbedRootScreen
const TabbedRootScreen(
{required this.navigationShell, required this.children, super.key});
{required this.shellState, required this.children, super.key});

/// The current state of the parent StatefulShellRoute.
final StatefulNavigationShell navigationShell;
final ShellRouteState shellState;

/// The children (branch Navigators) to display in the [TabBarView].
final List<Widget> children;
Expand All @@ -391,15 +382,17 @@ class TabbedRootScreen extends StatefulWidget {

class _TabbedRootScreenState extends State<TabbedRootScreen>
with SingleTickerProviderStateMixin {
ShellRouteState get _shellState => widget.shellState;

late final TabController _tabController = TabController(
length: widget.children.length,
vsync: this,
initialIndex: widget.navigationShell.currentIndex);
initialIndex: _shellState.navigatorIndex);

@override
void didUpdateWidget(covariant TabbedRootScreen oldWidget) {
super.didUpdateWidget(oldWidget);
_tabController.index = widget.navigationShell.currentIndex;
_tabController.index = _shellState.navigatorIndex;
}

@override
Expand All @@ -424,7 +417,7 @@ class _TabbedRootScreenState extends State<TabbedRootScreen>
}

void _onTabTap(BuildContext context, int index) {
widget.navigationShell.goBranch(index);
context.restore(_shellState.navigatorLocation(index));
}
}

Expand All @@ -446,6 +439,10 @@ class TabScreen extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Screen $label', style: Theme.of(context).textTheme.titleLarge),
Text(
'Shell navigator index: ${ShellRouteState.of(context).navigatorIndex}'),
Text(
'Root shell navigator index: ${ShellRouteState.of(context, name: 'rootShell').navigatorIndex}'),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ class RestorableStatefulShellRouteExampleApp extends StatelessWidget {
routes: <RouteBase>[
StatefulShellRoute.indexedStack(
restorationScopeId: 'shell1',
pageBuilder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
pageBuilder:
(BuildContext context, ShellRouteState state, Widget child) {
return MaterialPage<void>(
restorationId: 'shellWidget1',
child: ScaffoldWithNavBar(navigationShell: navigationShell));
child: ScaffoldWithNavBar(shellState: state, child: child));
},
branches: <StatefulShellBranch>[
// The route branch for the first tab of the bottom navigation bar.
Expand Down Expand Up @@ -105,27 +105,35 @@ class RestorableStatefulShellRouteExampleApp 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;

@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
body: child,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
],
currentIndex: navigationShell.currentIndex,
onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex),
currentIndex: shellState.navigatorIndex,
onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
),
);
}

void _onTabTap(BuildContext context, int index) {
context.restore(shellState.navigatorLocation(index));
}
}

/// Widget for the root/initial pages in the bottom navigation bar.
Expand Down
67 changes: 34 additions & 33 deletions packages/go_router/example/lib/stateful_shell_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
routes: <RouteBase>[
// #docregion configuration-builder
StatefulShellRoute.indexedStack(
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
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
Expand Down Expand Up @@ -142,55 +141,56 @@ 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;

// #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) {
context.restore(shellState.navigatorLocation(index));
}),
);
}
// #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.
if (index == shellState.navigatorIndex) {
shellState.resetNavigatorLocation(index);
}
context.restore(shellState.navigatorLocation(index));
}
}

Expand All @@ -217,7 +217,8 @@ class RootScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Root of section $label'),
title: Text(
'Root of section $label - navigatorIndex: ${ShellRouteState.of(context).navigatorIndex}'),
),
body: Center(
child: Column(
Expand Down
Loading
Loading