Skip to main content

Bootstrap & Dependency Injection

Bootstrap sequence

lib/main.dart is three lines:

void main() => bootstrap();

lib/bootstrap.dart:bootstrap() does all validation before rendering:

Future<void> bootstrap() async {
WidgetsFlutterBinding.ensureInitialized();
final config = AppConfig.parse(environmentValues); // collects *all* issues, throws AppConfigException
await registerDependencies(config);
runApp(KxApp(...));
}

If AppConfigException is caught, a DiagnosticScreen renders the issues: List<AppConfigIssue> with key, detail, rawValue so one run reports the whole misconfig — not one rebuild per bug.

AppConfig

lib/core/config/app_config.dart:

  • DataSource.fixture | remote
  • gracePeriodDays=30, chaosLatency=Duration.zero, chaosErrorRate=0, enableAnalyticsLog, enableDevMenu, storageNamespace, apiBaseUrl?
  • --dart-define keys: DATA_SOURCE, API_BASE_URL, GRACE_PERIOD_DAYS, CHAOS_LATENCY_MS, CHAOS_ERROR_RATE, ENABLE_ANALYTICS_LOG, ENABLE_DEV_MENU
  • Collects issues list then throws once; StorageNamespace.remote(apiBaseUrl) fingerprints namespace from URL.

AppConfig.fixtureDefaults() is used by tests and by the diagnostic screen so theme + localizations render even when config is invalid.

Service locator

GetIt is configured in lib/app/di/register_dependencies.dart:

Future<void> registerDependencies(AppConfig config) async {
final getIt = GetIt.instance;
getIt.registerSingleton<AppConfig>(config);
getIt.registerSingleton<KxDatabase>(await databaseConnection(config));
getIt.registerSingleton<LocalStore>(LocalStore(getIt<KxDatabase>()));
getIt.registerSingleton<AppScopeBindings>(AppScopeBindings.fromConfig(config));
// feature modules via DiModule chain
await FixtureFeatureModule(config).register(getIt); // or RemoteFeatureModule
}

lib/app/di/di_module.dart:DiModule is an extension-point — each feature appends its own register via getIt.registerFactory<...>.

lib/app/di/app_scope_bindings.dart holds long-lived Clock, IdGenerator (UuidIdGenerator / DartRandomSource), PreferencesService, AnalyticsStore.

Database connection

lib/core/database/database_connection.dart:

  • Web → drift_flutter with WasmDatabase or IndexedDb fallback.
  • Mobile/desktop → sqlite3_flutter_libs + path_provider.

Namespace isolation: Drift file name incorporates storageNamespace (hashed URL for remote), so fixture and remote(http://127.0.0.1:8000) never share rows.

Feature slots

lib/router/feature_slots.dart:FeatureSlots is the seam createAppRouter closes over:

class FeatureSlots {
Widget Function(BuildContext, KxRouteArgs) maintenanceHub;
Widget Function(...) inspection, charge, contest, photoViewer, statement, ...
}

Parts 5–7 replace slots via stewarded router composition; before they exist the shell renders inert placeholders.

Cubits in scope

lib/app/cubit/*:

  • BookingCubit — selected booking id + hub tab (HubTab.open | history) remembered per-booking.
  • ConnectivityCubitconnectivity_plus / online signal consumed by Outbox.
  • LocaleCubiten/cy, persisted via PreferencesService.
  • ThemeCubitsystem/light/dark, persisted.
  • NotificationsCubit — unread count for KxNavShell badge.
  • SyncCubitsyncing | idle | error for the progress indicator.

Clocks

  • SystemClock — wall clock.
  • AnchoredDemoClock — fixed demoNow (from config or seeded persisted_demo_clock).
  • ServerAdjustedClock — skews local now by serverTime - localTime delta observed from last meta.serverTime (used for deadline copy).

All deadlines use the injected Clock.now(); tests inject ManualClock and assert inclusive now >= deadlineAt.

Why this split

  • bootstrap owns environment only.
  • registerDependencies owns object graph only.
  • KxApp owns render tree only.

This makes the one-command flips (DATA_SOURCE=fixtureremote) a DI-only change and keeps widget tests free of async GetIt tear-down flakiness.