Frontend Module Atlas — What Is This & Every Module in Detail
kxinspect_frontend_flutter is a student-facing Flutter app for the KxInspections Contesting Charges interview assignment. A student lives in a booked property (BKG-001), receives an inspection report, is charged for damage/cleaning (CHG-001…), and must Accept or Contest (reason 10–2000 graphemes + 0–5 attachments) before a 30-day deadline auto-accepts. This atlas maps every Dart module — 187 files, ~22k LOC excluding generated kx_database.g.dart — so you can jump to the owner of any behavior.
Build profile: Flutter 3.41.9 / Dart 3.11.5,
bloc 9.1 + go_router 17.4 + drift 2.28 + dio 5.9, fixtures inassets/fixtures/, twoDataSourcemodes (fixturedefault,remoteover FastAPI). One commanddart format → flutter analyze → flutter test → goldensis the canonical gate.
Product snapshot (what is this)
| Visual state → route | Proves |
|---|---|
Maintenance Hub /maintenance?tab=open (HubTab.open) | booking selector, Outstanding blue banner, inventory/inspection/task/charge cards, bottom nav |
Maintenance Hub History ?tab=history | accepted/contested/resolved/paid with contest reason + statement link |
Inspection /inspections/:id | type•code•room title, general notes, ItemAction (charge?) vs ItemUpdate |
Charge /charges/:id | deadline copy, read-only fields, KxPhotoStrip → /photo/:i viewer, Contest/Accept/Pay |
Contest /charges/:id/contest | multiline Reason (UAX-29), file picker, optimistic submit, draft persistence |
Statement /statement | per-currency totals of unpaid, mock Pay accepted→paid |
Notifications /notifications | chargeRaised local/browser notification + unread badge |
:::tip Two invariants
Fixture-first — assets/fixtures/app_state.json is the byte-stable export of the Python seed; reviewers flutter run -d chrome --dart-define=DATA_SOURCE=fixture need no server. Thin UI — widgets never contain lifecycle logic; features/*/application + shared/domain own the 30-row transition table and deadlineAt = raisedAt + graceDays inclusive rule, replayed from Python vectors.
:::
flowchart TD
main[lib/main.dart → bootstrap.dart] --> app[lib/app\n cubits + DI + shell]
app --> router[lib/router\n go_router + FeatureSlots]
router --> features[lib/features\n charge / inspection / maintenance / statement / notifications]
features --> core[lib/core\n config / database / sync / network]
core --> drift[(Drift\n kx_database + LocalStore)]
core --> api[KxApi\n Dio vs Fixture adapter]
features --> ds[lib/design_system\n tokens + 18 components]
core --> shared[lib/shared\n domain + application]
shared --> l10n[lib/l10n\n en + cy ARB]
Top-level map
lib/
├── main.dart 5 LOC entry → bootstrap()
├── bootstrap.dart 66 parses AppConfig, registerDependencies, runApp(KxApp), diagnostic screen
├── app/ 13 files long-lived cubits, DI, shell
├── core/ 33 files config, Drift, sync, Dio, clocks
├── design_system/ 28 files tokens + theme + 18 components, zero business import
├── features/ 86 files vertical slices (application/data/presentation per feature)
├── router/ 5 files central route table + typed args
├── l10n/ 3 gen + 2 ARB en/cy strings
├── shared/ 14 files pure domain (lifecycle, money, ids) + Result/Failure
└── data/ (reserved)
1 · lib/main.dart & lib/bootstrap.dart
main.dart is 3 lines: void main() => bootstrap(); — keeps test harness import-free.
bootstrap.dart:bootstrap() does environment → graph → render in order:
WidgetsFlutterBinding.ensureInitialized()AppConfig.parse(environmentValues)— collects allAppConfigIssue{key,detail,rawValue}then throwsAppConfigException(one run reports every bad--dart-define). On throw, rendersDiagnosticScreeninstead ofKxAppso badAPI_BASE_URLnever silently falls back.await registerDependencies(config)— opens Drift, seeds from fixture bundle or snapshot.runApp(KxApp(router: createAppRouter(...)))
Ref: documentation/docs/frontend/bootstrap-di.md.
2 · lib/app/ — Application shell
| Submodule | Files | Responsibility |
|---|---|---|
app/kx_app.dart | KxApp | MaterialApp.router + AppBlocObserver, injects ThemeCubit/LocaleCubit/ConnectivityCubit |
app/shell/kx_app_shell.dart | KxAppShell | Scaffold with NavigationBar (compact) vs NavigationRail (≥840) via KxBreakpoints, HubTab badge |
app/cubit/booking | booking_cubit.dart | selectedBookingId + HubTab; per-booking rememberHubTab(tab) persisted via PreferencesService |
app/cubit/theme | theme_cubit.dart | ThemeMode.system/light/dark + system listener → KxTheme.light()/dark() |
app/cubit/locale | locale_cubit.dart | Locale('en'/'cy') persisted, rebuilds MaterialApp.locale |
app/cubit/connectivity | connectivity_cubit.dart | online/offline from connectivity_plus + navigator.onLine web → Outbox drain trigger |
app/cubit/sync | sync_cubit.dart | idle/syncing/error for KxProgressIndicator |
app/cubit/notifications | notifications_cubit.dart | unreadCount derived from LocalStore.notificationsStream → KxNavShell badge |
app/di/register_dependencies.dart | GetIt composition root: AppConfig → KxDatabase → LocalStore → AppScopeBindings | |
app/di/app_scope_bindings.dart | holds Clock, IdGenerator, PreferencesService, AnalyticsStore | |
app/di/di_module.dart | DiModule extension point each feature appends | |
app/di/fixture_feature_module.dart | registers FixtureHttpClientAdapter + ChargeRepositories.fixture when DataSource.fixture | |
app/app_bloc_observer.dart | global onChange → maybeLogBlocTransition when ENABLE_ANALYTICS_LOG=true | |
app/feature_composition.dart | stitches FeatureSlots for router |
Rule: app never imports features directly — it depends on ports, features inject via DiModule.
3 · lib/core/ — Platform & infrastructure
3.1 core/config (3 files)
app_config.dart(395 LOC) —DataSource.fixture|remote,gracePeriodDays=30,chaosLatency/ErrorRate,enableAnalyticsLog/DevMenu,storageNamespace,apiBaseUrl. KeysDATA_SOURCE, API_BASE_URL, GRACE_PERIOD_DAYS, CHAOS_LATENCY_MS, CHAOS_ERROR_RATE, ENABLE_….StorageNamespace.remote(uri)hashes URL sofixtureandremote(http://…)Drift files never collide.contract_version.dart—contractVersion=1,schemaVersion=1constants.storage_namespace.dart— namespace fingerprint helper.
3.2 core/database (11 files, ~18.5k LOC inc. generated)
| File | Role |
|---|---|
kx_database.dart (407) | Drift tables: Bookings, InventoryReports, Inspections, Tasks, Charges, ChargePhotos, ContestDrafts, AttachmentBlobs, OutboxRows, SyncMeta (DataClassName, FK cascade) |
kx_database.g.dart (15 890) | generated companions, DAOs |
local_store.dart (898) | transactional façade: bookingsStream(), hubSnapshotStream(id), chargesStream(), applySnapshot(), applyRemoteChanges(), enqueueOutbox() |
domain_mapper.dart (612) | Drift ↔ shared/domain strict mapping |
fixture_bundle.dart (241) | loads assets/fixtures/app_state.json + manifest.json + media |
database_connection.dart (43) | drift_flutter Wasm/IndexedDB (web) vs sqlite3_flutter_libs + path_provider (mobile/desktop), file name from StorageNamespace |
repository_stream.dart (46) | broadcast Stream<List<T>> helpers |
persisted_demo_clock.dart (61) | pins referenceNow 2026-08-01T12:00:00Z for deadline demo |
attachment_bytes*.dart (3) | dart:io vs dart:html conditional byte read + SHA-256 |
Ref: documentation/docs/frontend/data-layer.md.
3.3 core/network (4 files)
kx_api.dart(448) — abstractKxApi{ health(), hub(), charges(), charge(), accept(), contest(), pay(), taskCreate(), events() }withIdempotency-Key: uuidper command.api_models.dart(79) —json_serializableDTOs mirroringapp/schemasstrict lowerCamelCase.fixture_http_client_adapter.dart(101) — replays bundle, fabricatesstateEpoch/storeRevisiondeterministically.failure_mapper.dart+exception_mapper.dart—DioException → KxFailuretyped union.
Swapping DataSource swaps only this adapter — no widget changes.
3.4 core/sync (12 files)
| File | LOC | Role |
|---|---|---|
outbox.dart | 506 | Drift OutboxRows{ opKind: accept/contest/pay/taskCreate, payloadJson, stateEpoch, version, attempts, syncState}; enqueue is always insert+optimistic Charges flip, drain() on online |
sync_service.dart | 173 | dequeues createdAt order, fresh Idempotency-Key per attempt, store.epoch_mismatch→SnapshotCoordinator.refresh(), 429→RetryPolicy |
retry_policy.dart | 27 | exponential 300 ms→8 s + jitter ±20% |
live_sync_service.dart | 85 | KxApi.events(lastEventId) ResponseType.stream → SSE parse → SseEntityCommitter |
sse_entity_committer.dart | 97 | patches single Charges row transactionally from charge.updated |
snapshot_coordinator.dart | 113 | owns SyncMeta{stateEpoch,storeRevision,streamEpoch,streamCursor}, seeds from fixture vs GET /sync-snapshot, refetches on sync.required |
server_adjusted_clock.dart | 96 | skew = serverTime - now() per meta.serverTime → correct deadlineAt copy |
sync_leadership*.dart | 49+ | BroadcastChannel leader election (web) vs single owner (native) — one tab drains SSE |
Ref: documentation/docs/frontend/offline-sync.md.
3.5 core/time (3 files)
anchored_demo_clock.dart (31), system_clock.dart (13), timer_scheduler.dart (56) — Clock/Scheduler abstraction so tests inject ManualClock at fixed instant for inclusive now ≥ deadlineAt.
3.6 core/analytics + core/error + core/storage + core/platform
analytics_store/sinks/validator/route_observer(331) — typed catalogmock.charge.accept, route.view;AppBlocObserver+AnalyticsRouteObserverproduce.exception_mapper(47) — contracterror.code → KxFailure.preferences_service(243) —SharedPreferenceswrapper for cubits.uuid_id_generator/dart_random_source(30) —GetItIdGeneratordeterministic in tests.
4 · lib/design_system/ — Zero-business component kit (barrel design_system.dart)
Foundations (foundations/ 9 files)
kx_colors (semantic ThemeExtension), kx_palette (raw ramps, tealAccentDecorative=#26BFA8 decorative-only), kx_spacing (4-pt), kx_radius, kx_typography (Roboto 400/500/700), kx_breakpoints (compact < 600, medium 600–839, expanded 840–1439, large >= 1440, allowsTwoColumns >= 840), kx_motion (150/250/350 ms, collapses to 0 when disableAnimations), kx_elevation (level1/2/3 + minTouchTarget 48), kx_tone (product tone, no domain meaning).
Theme
theme/kx_theme.dart — KxTheme.light()/dark() maps tokens to ThemeData for app bar, card, nav bar/rail, tabs, checkbox/radio/switch, progress, dialog, tooltip, page transitions. Mode owned by ThemeCubit.
Components (components/ 18)
kx_button (filled/outlined/text + loading), kx_status_pill, kx_banner (info/success/warning + liveRegion), kx_card, kx_section_header, kx_icon_text, kx_readonly_field, kx_selector_field, kx_tabs/KxTabItem, kx_photo_strip/KxPhotoItem (the only horizontal scroller), kx_image (bounded decode, fallback), kx_skeleton/KxSkeletonBlock (shimmer stops under reduced motion), kx_message_state (empty/error), kx_content_container, kx_hero_app_bar (sliver — compose with CustomScrollView), kx_nav_shell, kx_focus_ring, kx_progress_indicator.
Guards: test/design_system/design_system_purity_test.dart forbids raw Colors.*, hardcoded prose, and any features import. Goldens test/golden/design_system/ (10 baselines, DPR 1.0, NoSplash) — Linux CI must regenerate once (macOS vs Linux raster drift).
Ref: documentation/docs/frontend/design-system.md.
5 · lib/features/ — Vertical slices
Each feature is application/ (pure → ports) → data/ (repo impl + DTOs) → presentation/ (widgets + blocs/pages). Widgets never contain transition() logic.
5.1 features/maintenance (22 files) — Hub /maintenance?tab=open|history
application/ports/maintenance_ports.dart,data/repositories/maintenance_repositories.dart—MaintenancePorts{ hubSnapshotStream, tasksForBooking }backed byLocalStore.data/sync/task_command_handler.dart— enqueuestaskCreateviaOutbox(client UUID authoritative).presentation/bloc/maintenance_hub/,presentation/bloc/report/—MaintenanceHubBlocsubscribes tohubSnapshotStreamand derivesOpen = outstanding|accepted|contested(bydeadlineAtasc) vsHistory = accepted|contested|resolved|paid(by last event desc,acceptedon both), plus reports.presentation/widgets/booking_selector, hub_banner_view, hub_cards, hub_section— mapsChargeStatus→KxTone, usesKxBanner.info/KxCard.presentation/navigation/maintenance_navigator.dart—goNamed(inspection/charge)fromchargeId.
Ref: documentation/docs/frontend/features/maintenance-hub.md.
5.2 features/inspection (10 files) — /inspections/:id
application/ports+data/repositories— singleinspectionStream(id).presentation/bloc/inspection_detail,presentation/view/inspection_view_data— composesItemAction{ chargeId?+amountMinor? }vsItemUpdate.presentation/widgets/inspection_cards.dart—KxCardisonTaponly whenchargeId != null; otherwise no semanticsbutton.navigation/inspection_navigator.dart— charged-item route.
Ref: documentation/docs/frontend/features/inspection.md.
5.3 features/charge (32 files) — core lifecycle /charges/:id + /contest + /photo/:i
application/contest/contest_policy.dart—Characters(reason).length ∈ [10,2000](mirrors Pythongraphemes.py, tested againstcharge_state_vectors.jsonincluding 🇬🇧 flag, ZWJ, CR LF).application/contest/contest_draft.dart— debounce 300 ms →ContestDrafts(chargeId, reason)upsert, survives reload.application/contest/charge_concurrency.dart— snapshotsexpectedStateEpoch + expectedVersionat tap-time forKxApi.application/ports/charge_ports.dart—accept/contest/pay/contestDraftport;data/repositories/charge_repositories.dart+data/sync/charge_command_handler.dartimplement viaOutbox + LocalStoreoptimisticaccepted/contestedflip.data/attachments/{database_contest_draft_store, file_picker_attachment_gateway}—file_picker+ MIME whitelist (jpeg/png/webp/mp4/pdf, ≤10 MiB/file, ≤25 MiB total) matched to backend.presentation/bloc/charge_detail+presentation/bloc/contest_form—ChargeDetailBlocwatcheschargeStream(id),ContestFormBlocdisables Submit until ≥10 graphemes.presentation/pages/{charge_detail_page, contest_charge_page, charge_photo_viewer_page}+widgets/{charge_action_bar, charge_banner, charge_details_section, charge_evidence_section, evidence_viewer, contest_*}—KxPhotoStrip→InteractiveViewer(pinch/web wheel), invalidphotoIndex→?notice=photoIndexRepairedredirect.
Ref: documentation/docs/frontend/features/charge-contest.md.
5.4 features/statement (3 files) — /statement
application/statement_models.dart (per-currency Map<currency, amountMinor> over unpaid outstanding|contested|accepted), presentation/bloc/statement_bloc.dart, presentation/pages/statement_page.dart — mock pay ( accepted→paid sequential keys, no batch endpoint).
Ref: documentation/docs/frontend/features/statement.md.
5.5 features/notifications (9 files) — /notifications bonus B-02
application/{notification_coordinator, notification_event, ports/notification_ports},data/{notification_feed_repository, notification_store},platform/{gateway_io, _stub, _web}—flutter_local_notifications(native) + Web Notifications API, both foreground-only from SSEcharge.created.presentation/bloc/notifications_bloc.dart+presentation/pages/notifications_page.dart—readisPOST .../readwithexpectedStateEpoch, naturally idempotent.
Ref: documentation/docs/frontend/features/notifications.md.
5.6 features/settings + features/analytics_debug
s/settings/presentation/pages — ThemeCubit/LocaleCubit toggles (+ ENABLE_DEV_MENU dev route list). features/analytics_debug/presentation/pages/analytics_debug_page.dart — log view when ENABLE_ANALYTICS_LOG=true.
6 · lib/router/ — Single route table
| File | Role |
|---|---|
app_router.dart (244) | createAppRouter(config, cubits, FeatureSlots) — GoRouter with ShellRoute(KxAppShell), GoRoute(root)→maintenance?tab=open, maintenance tab-repair redirect, report/inspection/charge typed Id.tryParse → RouteErrorPage, charge children contest + photoViewer index-repair (-1→?notice=photoIndexRepaired) |
route_names.dart (142) | KxRoutes{ rootPath, maintenancePath, reportPath, inspectionPath, chargePath, contestRelativePath, photoViewerRelativePath, statementPath } + param constants tabQueryParameter, chargeIdParameter, photoIndexParameter, noticeQueryParameter |
route_args.dart (106) | KxRouteArgs{ maintenance(tab), report(id), inspection(id), charge(id,notice), contest(id), photoViewer(id,index), statement(id?) } + HubTab.tryParse/fallback, RouteNotice |
feature_slots.dart (113) | FeatureSlots{ maintenanceHub, report, inspection, charge, contest, photoViewer, statement, notifications, settings } builder map — router never imports feature pages |
route_error_page.dart (61) | non-localized 404 with heading semantics |
| Analytics | core/analytics/analytics_route_observer.dart emits route.view |
Ref: documentation/docs/frontend/routing.md. Deep links verified via flutter test test/router/ + integration tester.goNamed(KxRoutes.chargeName, {chargeId: CHG-001}).
7 · lib/l10n/ + lib/shared/
l10n/app_en.arb + app_cy.arb→l10n/app_localizations*.dart(generated,flutter gen-l10n). Every user string in ARB; inspector prose (notes, contestReason) never translated.titleKey/bodyKey/altKeyfrom backend remain keys. Booking selectorMM/DD/YYYYis literal; other datesintl DateFormatlocale-sensitive.design_systemholds no strings (purity test).shared/domain/{ids, enums, money, entities, charge_lifecycle, deadline_policy, hub_snapshot, commands, storage_namespace}— typedBookingId/ChargeId/InspectionId,ChargeStatus, ChargeType, RecordStatus,Money{amountMinor,currency},charge_lifecycle.transition()total function,deadline_policyinclusivenow≥deadlineAt,freezedunionsResult/Success/Failure.shared/application/{result, failure, ports}—Result<T>/KxFailureused by ports.
Ref: documentation/docs/frontend/l10n-theming.md.
8 · assets/ + tool/ + test/
assets/fixtures/{app_state.json, manifest.json, media/att_*.png/.t.png,att_rpt_*.pdf, vectors/{charge_state,deadline,namespace}_vectors.json}— committed bundle copied fromkxinspect_backend_python/scripts/export_fixtures.py; verified byverify_fixture_manifest.pyparity.assets/fonts/Roboto-{Regular,Medium,Bold}.ttf+ASSET_PROVENANCE.md,assets/images/.gitkeep.tool/test_all.sh(canonical gate),tool/android_runtime_proof.sh,tool/aggregate_evidence.dart/check_evidence.dart(SHA manifest).test/{unit, widget, golden, foundation, router, architecture}+integration_test/app_test.dart— unit: vectors+deadline+graphemes; widget: 360/600/840/1440 × 1.0/1.3/2.0 × LTR/RTL × light/dark, Enter/Space, focus ring, semantics; golden: 10 design baselines (DPR 1.0, pinned harness); integration: fixture boot → hub → inspection → charge → contest → history → statement → pay → reload persistence.
Where to read next
- New to the codebase?
lib/bootstrap.dart → app/di/register_dependencies.dart → router/app_router.dart. - Adding a feature? Copy
features/charge/{application,data,presentation}vertical slice; keepdesign_systembelow. - Changing a token? Touch only
design_system/foundations+theme/kx_theme.dartand updatetest/design_system. - Swapping datasource? Change only
DataSource--dart-define— DI swapsKxApiadapter, Drift streams + widgets unchanged.
Live frontend docs: documentation/docs/frontend/{overview, architecture, bootstrap-di, routing, state-management, design-system, data-layer, offline-sync}.