Routing
Single source: lib/router/app_router.dart:createAppRouter. Built on go_router 17.4. All navigation uses typed KxRoutes names; direct URL load and browser back are first-class.
Route table
| Name | Path | Args | Notes |
|---|---|---|---|
root | / | — | redirects to maintenance?tab=open |
maintenance | /maintenance | `?tab=open | history` |
report | /reports/:reportId | ReportId | inventory report detail |
inspection | /inspections/:inspectionId | InspectionId | detail + charged item routing |
charge | /charges/:chargeId | ChargeId, RouteNotice? | banner/card entry |
contest | /charges/:chargeId/contest | ChargeId | child of charge |
photoViewer | /charges/:chargeId/photo/:photoIndex | ChargeId, int >=0 | child of charge, thumb deep link |
statement | /statement?chargeId=CHG-... | ChargeId? | unpaid totals + mock Pay |
notifications | /notifications | — | feed |
settings | /settings | — | theme/locale toggles + dev menu |
KxRoutes in lib/router/route_names.dart carries typed path templates + query param constants (tabQueryParameter, noticeQueryParameter). KxRouteArgs in route_args.dart is the strongly-typed GoRouterState extraction result, and RouteNotice (photoIndexRepaired) signals a redirect repaired an invalid index.
Shell & redirects
GoRouter(
initialLocation: state.namedLocation(KxRoutes.maintenanceName, queryParameters: {tab: HubTab.open.name}),
routes: [
GoRoute(root, redirect: → maintenance?tab=open),
ShellRoute(builder: (c,s,child) => KxAppShell(location: s.uri, child: child), routes: [
GoRoute(maintenance, redirect: repair invalid tab, builder: featureSlots.maintenanceHub),
GoRoute(charge, routes: [contest, photoViewer]),
]),
GoRoute(routeError, path: /404)
],
)
- Maintenance tab repair — if
?tabraw is null/unknown,HubTab.tryParse→fallback(openorbookingCubit.hubTabForSelectedBooking()) with redirect preserving location. - Photo index repair —
GET /charges/CHG-001/photo/-1redirects tochargewith?notice=photoIndexRepaired. - Invalid id — any
ChargeId.tryParsefailure rendersRouteErrorPage(semanticsheading).
FeatureSlots
FeatureSlots (lib/router/feature_slots.dart) maps each KxRouteArgs variant to a widget builder. The router imports no feature page; features register by replacing the slot. This keeps app/router dependency acyclic (router→ports, features→router).
Analytics hook
AnalyticsRouteObserver (lib/core/analytics/analytics_route_observer.dart) listens to GoRouter and emits analytics_store events per route change; validated by analytics_validator.
Deep-link contract
Bonus B-08 deep links must work headless:
router.goNamed(KxRoutes.chargeName, pathParameters: {chargeIdParameter: "CHG-001"});
await tester.pumpAndSettle(); // 200
router.goNamed(KxRoutes.contestName, pathParameters: {chargeIdParameter: "CHG-001"});
Web SPA fallback is configured via web/canonical and netlify.toml redirects so direct loads don't 404.
Navigation idioms
// preferred — named, typed
context.goNamed(KxRoutes.inspectionName, pathParameters: {KxRoutes.inspectionIdParameter: id.value});
// imperative when args required
context.pushNamed(KxRoutes.photoViewerName, pathParameters: {..., photoIndexParameter: index.toString()});
No raw string paths; forbidden API test rejects Navigator.push.
Testing
test/router/app_router_test.dart asserts:
- root redirects,
- tab repair idempotent,
- photo index repair,
- charged item routes, uncharged doesn't,
Backrestores hub with same tab/booking,- English/Welsh parity at each route.
Direct initialLocation injection + overridePlatformDefaultLocation allow each case to start without building the whole app.