Skip to main content

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

NamePathArgsNotes
root/redirects to maintenance?tab=open
maintenance/maintenance`?tab=openhistory`
report/reports/:reportIdReportIdinventory report detail
inspection/inspections/:inspectionIdInspectionIddetail + charged item routing
charge/charges/:chargeIdChargeId, RouteNotice?banner/card entry
contest/charges/:chargeId/contestChargeIdchild of charge
photoViewer/charges/:chargeId/photo/:photoIndexChargeId, int >=0child of charge, thumb deep link
statement/statement?chargeId=CHG-...ChargeId?unpaid totals + mock Pay
notifications/notificationsfeed
settings/settingstheme/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 ?tab raw is null/unknown, HubTab.tryParsefallback (open or bookingCubit.hubTabForSelectedBooking()) with redirect preserving location.
  • Photo index repairGET /charges/CHG-001/photo/-1 redirects to charge with ?notice=photoIndexRepaired.
  • Invalid id — any ChargeId.tryParse failure renders RouteErrorPage (semantics heading).

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.

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.

// 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,
  • Back restores 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.