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 | remotegracePeriodDays=30,chaosLatency=Duration.zero,chaosErrorRate=0,enableAnalyticsLog,enableDevMenu,storageNamespace,apiBaseUrl?--dart-definekeys: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_flutterwithWasmDatabaseorIndexedDbfallback. - 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.ConnectivityCubit—connectivity_plus/onlinesignal consumed byOutbox.LocaleCubit—en/cy, persisted viaPreferencesService.ThemeCubit—system/light/dark, persisted.NotificationsCubit— unread count forKxNavShellbadge.SyncCubit—syncing | idle | errorfor the progress indicator.
Clocks
SystemClock— wall clock.AnchoredDemoClock— fixeddemoNow(from config or seededpersisted_demo_clock).ServerAdjustedClock— skews localnowbyserverTime - localTimedelta observed from lastmeta.serverTime(used for deadline copy).
All deadlines use the injected Clock.now(); tests inject ManualClock and assert inclusive now >= deadlineAt.
Why this split
bootstrapowns environment only.registerDependenciesowns object graph only.KxAppowns render tree only.
This makes the one-command flips (DATA_SOURCE=fixture ↔ remote) a DI-only change and keeps widget tests free of async GetIt tear-down flakiness.