/// UI-01: a persistent 4-tab shell (`StatefulShellRoute`), replacing the push/pop stack /// rooted at Record that this app used through v3. Each branch keeps its own navigator, /// so pushing Trip Detail from Rides -- or the route planner from Plan -- doesn't /// disturb the other tabs' state or the active tab index. library; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'app_shell.dart'; import 'detail/trip_detail_screen.dart'; import 'record/record_screen.dart'; import 'routes/route_planner_screen.dart'; import 'routes/routes_list_screen.dart'; import 'settings/settings_screen.dart'; import 'trips/trips_screen.dart'; abstract final class Routes { static const record = '/'; static const trips = '/rides'; static const tripDetail = '/rides/:tripId'; static const settings = '/settings'; static const routes = '/plan'; static const routePlanner = '/plan/:routeId'; static String detailFor(int tripId) => '/rides/$tripId'; static String plannerFor(int routeId) => '/plan/$routeId'; } GoRouter buildRouter() => GoRouter( initialLocation: Routes.record, routes: [ StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => AppShell(navigationShell: navigationShell), branches: [ // Branch 0: Map. The Record screen, headerless -- see AppShell's doc comment. StatefulShellBranch( routes: [ GoRoute(path: Routes.record, builder: (context, state) => const RecordScreen()), ], ), // Branch 1: Rides. Trip Detail pushes within this branch's own navigator. StatefulShellBranch( routes: [ GoRoute( path: Routes.trips, builder: (context, state) => TripsScreen( onOpenTrip: (id) => context.push(Routes.detailFor(id)), ), routes: [ GoRoute( path: ':tripId', builder: (context, state) { final id = int.tryParse(state.pathParameters['tripId'] ?? ''); if (id == null) return const _NotFound(); return TripDetailScreen(tripId: id, onBack: () => context.pop()); }, ), ], ), ], ), // Branch 2: Plan. The route planner pushes within this branch's own navigator. StatefulShellBranch( routes: [ GoRoute( path: Routes.routes, builder: (context, state) => RoutesListScreen( onOpenRoute: (id) => context.push(Routes.plannerFor(id)), ), routes: [ GoRoute( path: ':routeId', builder: (context, state) { final id = int.tryParse(state.pathParameters['routeId'] ?? ''); if (id == null) return const _NotFound(); return RoutePlannerScreen(routeId: id, onBack: () => context.pop()); }, ), ], ), ], ), // Branch 3: Settings. StatefulShellBranch( routes: [ GoRoute( path: Routes.settings, builder: (context, state) => const SettingsScreen(), ), ], ), ], ), ], errorBuilder: (context, state) => const _NotFound(), ); class _NotFound extends StatelessWidget { const _NotFound(); @override Widget build(BuildContext context) => Scaffold( body: Center( child: TextButton( onPressed: () => context.canPop() ? context.pop() : context.go(Routes.record), child: const Text('That ride could not be found. Back'), ), ), ); }