| j / k | Scroll down / up |
| gg | Scroll to top |
| G | Scroll to bottom |
| ⌘K / / | Open search |
| ? | Show this help |
| Esc | Close search / help |
| n / N | Next / previous section |
| h | Go to landing |
| d | Go to docs |
| : | Command mode — type section name |
Press ? or Esc to close
Feature defines its own route. App collects all routes. Navigate via enum — never hardcoded strings.
final profileRoute = GoRoute(
path: AppRoutes.profile.route,
name: AppRoutes.profile.name,
builder: (context, state) => const ProfileScreen(),
routes: [
GoRoute(
path: AppRoutes.settings.route,
name: AppRoutes.settings.name,
parentNavigatorKey: shellNavigatorKey,
builder: (context, state) => const SettingsScreen(),
),
],
); final appRouter = GoRouter(
initialLocation: AppRoutes.splash.route,
navigatorKey: rootNavigatorKey,
redirect: (context, state) async {
final noAuthRoutes = [
AppRoutes.splash.route,
AppRoutes.webView.route,
AppRoutes.mediaViewer.route,
];
if (noAuthRoutes.contains(state.uri.path)) return null;
final token = await sl<TokenManager>().getAccessToken();
final isAuthenticated = token != null && token.isNotEmpty;
final isAuthRoute = state.uri.path.startsWith(AppRoutes.auth.route);
if (!isAuthenticated && !isAuthRoute) return AppRoutes.auth.route;
if (isAuthenticated && isAuthRoute) return AppRoutes.home.route;
return null;
},
routes: [
GoRoute(path: AppRoutes.splash.route, builder: ...),
authRoute,
StatefulShellRoute.indexedStack(
builder: (context, state, shell) => _AppShell(shell),
branches: [
StatefulShellBranch(routes: [homeRoute]),
StatefulShellBranch(routes: [feedRoute]),
StatefulShellBranch(routes: [discoverRoute]),
StatefulShellBranch(routes: [activityRoute]),
StatefulShellBranch(routes: [profileRoute]),
],
),
],
); // Never hardcoded strings — always enum
context.go(AppRoutes.profile.route);
context.push(AppRoutes.settings.route);
// With parameters
context.push(AppRoutes.profileView.route, pathParameters: {'user_id': userId}); Five top-level tabs, each its own StatefulShellBranch. The shell owns the Scaffold and AppBottomNavBar; switching tabs calls goBranch so each branch keeps its own navigation stack.
final appRouter = GoRouter(
initialLocation: AppRoutes.splash.route,
navigatorKey: rootNavigatorKey,
redirect: _authGuard,
routes: [
GoRoute(path: AppRoutes.splash.route, builder: ...),
authRoute,
StatefulShellRoute.indexedStack(
builder: (context, state, shell) => _AppShell(shell),
branches: [
StatefulShellBranch(routes: [homeRoute]),
StatefulShellBranch(routes: [feedRoute]),
StatefulShellBranch(routes: [discoverRoute]),
StatefulShellBranch(routes: [activityRoute]),
StatefulShellBranch(routes: [profileRoute]),
],
),
],
);
class _AppShell extends StatefulWidget {
const _AppShell(this.navigationShell);
final StatefulNavigationShell navigationShell;
@override
State<_AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<_AppShell> {
void _onTabTapped(int index) {
if (index == widget.navigationShell.currentIndex) return;
widget.navigationShell.goBranch(index);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: widget.navigationShell,
bottomNavigationBar: AppBottomNavBar(
currentIndex: widget.navigationShell.currentIndex,
onTap: _onTabTapped,
items: AppBottomNavItem.values.toList(),
),
);
}
}