appRoutes top-level property
App routes configuration using StatefulShellRoute for state preservation
Routes structure:
- /login (standalone, no shell)
- /game/:gameId (standalone, no shell, full-screen)
- StatefulShellRoute (with drawer, preserves state):
- Branch 0: /home
- Branch 1: /lobby
- Branch 2: /history
- Branch 3: /social
- Branch 4: /about
- Branch 5: /settings
- /settings/profile
Implementation
final List<RouteBase> appRoutes = [
// The web app is hosted at the game origin. Give its root a real route so a
// cold load does not depend on the router's exception fallback.
GoRoute(path: '/', redirect: (context, state) => '/home'),
// Login route - outside shell (no drawer)
GoRoute(
path: '/login',
name: 'login',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) => const LoginScreen(),
),
// Game route - outside shell (parentNavigatorKey: rootNavigatorKey) so it
// covers the shell entirely; pushed onto the root navigator so back returns
// to the source screen (home, lobby, or history).
GoRoute(
path: '/game/:gameId',
name: 'game',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) {
final gameId = state.pathParameters['gameId']!;
return GameScreen(gameId: gameId);
},
routes: [
// Replay - full-screen over the game screen (root navigator), so back
// returns to the finished game. Used both by a participant replaying
// their own game and by a non-participant replaying a public one.
GoRoute(
path: 'replay',
name: 'replay',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) {
final gameId = state.pathParameters['gameId']!;
return ReplayScreen(gameId: gameId);
},
),
],
),
// Join route - handles deep linking or manual code entry
GoRoute(
path: '/join/:code',
name: 'join',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) {
final code = state.pathParameters['code']!;
return JoinGameScreen(code: code);
},
),
// Stateful shell route - preserves state across branch switches
StatefulShellRoute.indexedStack(
parentNavigatorKey: rootNavigatorKey,
builder: (context, state, navigationShell) {
// ShellScaffold now receives NavigationShell for built-in navigation
return ShellScaffold(navigationShell: navigationShell);
},
branches: [
// Branch 0: Home
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomeScreen(),
),
],
),
// Branch 1: Lobby
StatefulShellBranch(
routes: [
GoRoute(
path: '/lobby',
name: 'lobby',
builder: (context, state) => const LobbyScreen(),
),
],
),
// Branch 2: History
StatefulShellBranch(
routes: [
GoRoute(
path: '/history',
name: 'history',
builder: (context, state) => const HistoryScreen(),
),
],
),
// Branch 3: Social
StatefulShellBranch(
routes: [
GoRoute(
path: '/social',
name: 'social',
builder: (context, state) => const SocialScreen(),
),
],
),
// Branch 4: About
StatefulShellBranch(
routes: [
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => const AboutScreen(),
),
],
),
// Branch 5: Settings
StatefulShellBranch(
routes: [
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => const SettingsScreen(),
routes: [
// Profile is a full-screen edit flow: it escapes the shell
// intentionally so the drawer is not accessible while editing.
GoRoute(
path: 'profile',
name: 'profile',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) => const ProfileScreen(),
),
// Under settings rather than its own tab: a game that sells
// nothing should not grow a navigation destination, and the
// screen says so plainly when it is reached anyway.
GoRoute(
path: 'store',
name: 'store',
builder: (context, state) => const StoreScreen(),
),
],
),
],
),
],
),
];