--- phase: 01-project-infrastructure-module-wiring plan: 04 subsystem: client-bootstrap tags: [koin, kermit, di, logging, ios-bridge, android-application, wasm-bootstrap] requires: - 01-02 (build-logic conventions providing Koin + Kermit dependencies via recipe.kotlin.multiplatform) - 01-03 (composeApp/build.gradle.kts wired to convention plugin + libs.koin.android in androidMain) provides: - "initKoin(config: KoinAppDeclaration?): KoinApplication — single bootstrap helper" - "appModule: Koin Module — empty placeholder; Phase 2+ extends with authModule, syncModule, catalogModule" - "configureLogging() — sets Kermit Logger.setTag(\"recipe\")" - "KoinIosKt.doInitKoin() — Swift-callable iOS bridge" - "MainApplication: Android Application subclass invoking configureLogging + initKoin on process boot" affects: - "All future phases (2-11) plug Koin modules into appModule and call Logger.x { } via Kermit" - "Phase 2 (Auth) will register authModule; Phase 4 (SyncEngine) will register syncModule singleton" tech-stack: added: [] patterns: - "Single initKoin() call site per platform entry point (PITFALL #4 — no double-init on iOS)" - "configureLogging() ALWAYS precedes initKoin() so Koin module loading can use Kermit" - "App.kt (@Composable) NEVER calls startKoin (Pattern 4 anti-pattern guard)" - "iOS Kotlin bridge: top-level fun doInitKoin in KoinIos.kt → Swift symbol KoinIosKt.doInitKoin" - "Wasm init order: configureLogging → initKoin → ComposeViewport (PITFALL #8)" key-files: created: - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt - composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt modified: - composeApp/src/androidMain/AndroidManifest.xml - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt - iosApp/iosApp/iOSApp.swift unchanged_by_design: - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt # anti-pattern guard: no startKoin in @Composable - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt # PITFALL #4: Koin started exclusively in iOSApp.init() - iosApp/iosApp/ContentView.swift # already wraps MainViewControllerKt.MainViewController() decisions: - "Kermit tag = \"recipe\" (D-15) — exact string" - "appModule is empty in Phase 1 (D-14); Phase 2+ adds modules" - "Single iOS Koin call site is iOSApp.init() (PITFALL #4 mitigation)" - "androidContext(this@MainApplication) — qualified `this` because initKoin lambda receiver is KoinApplication" metrics: tasks_completed: 3 tasks_total: 3 files_created: 5 files_modified: 4 duration: ~10m completed: 2026-04-24 --- # Phase 1 Plan 4: Koin + Kermit Bootstrap Wiring — Summary Wired the Koin DI container and Kermit structured logger across all four composeApp platform entry points (Android Application subclass, iOS SwiftUI App.init, JVM desktop main, Wasm browser main) with a single `initKoin()` helper in commonMain and an empty `appModule` placeholder that Phase 2+ extends. ## What was built ### Task 1 — commonMain DI + logging + iOS bridge (commit `cc5002d`) Created four files: - **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt`** — exports `fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin { config?.invoke(this); modules(appModule) }`. The optional `config` lambda is how Android passes `androidContext(...)` and how Phase 2+ tests can inject overrides without touching the helper itself. - **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`** — declares `val appModule = module { }` (empty per D-14). Phase 2 adds `authModule`, Phase 4 adds `syncModule`, etc. - **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt`** — `fun configureLogging() { Logger.setTag("recipe") }`. Kermit's per-platform writers (OSLog/LogCat/println) install themselves by default; setting the tag is the only required call. - **`composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt`** — `fun doInitKoin() { configureLogging(); initKoin() }`. The top-level `fun` in file `KoinIos.kt` becomes the Swift-accessible symbol `KoinIosKt.doInitKoin()` automatically (Kotlin/Native generates `Kt` for top-level decls). ### Task 2 — Android MainApplication + manifest (commit `8cd608a`) - **`composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt`** — `class MainApplication : Application()` whose `onCreate()` calls `super.onCreate()`, then `configureLogging()`, then `initKoin { androidContext(this@MainApplication) }`. Qualified `this@MainApplication` is required because the `initKoin { }` lambda receiver is `KoinApplication`, not the `Application`. - **`composeApp/src/androidMain/AndroidManifest.xml`** — added `android:name=".MainApplication"` as the first attribute on ``. All other attributes and the ``/`` subtree preserved verbatim. ### Task 3 — JVM + Wasm + Swift entry points (commit `fd3e7e1`) - **`composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt`** — converted `fun main() = application { ... }` (single-expression) into a body block: `configureLogging()` → `initKoin()` → `application { Window(title = "recipe") { App() } }`. Window title and exit handler preserved. - **`composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt`** — same init order before `ComposeViewport { App() }`. `@OptIn(ExperimentalComposeUiApi::class)` retained. Defensive against PITFALL #8 (Wasm composition running before DI is ready) — Phase 1 has no ViewModels so the symptom would not surface yet, but the shape is correct from day 1. - **`iosApp/iosApp/iOSApp.swift`** — added `import ComposeApp` (matches framework basename set by `recipe.kotlin.multiplatform`) and `init() { KoinIosKt.doInitKoin() }`. The `WindowGroup { ContentView() }` body is unchanged. `MainViewController.kt` and `ContentView.swift` were intentionally NOT modified — Koin is bootstrapped exclusively from `iOSApp.init()` (PITFALL #4 mitigation). ## Init order invariant (every platform) ``` configureLogging() → installs Kermit tag "recipe" initKoin() → starts Koin with empty appModule [platform composition entry — application { } / ComposeViewport { } / ComposeUIViewController { } / setContent { }] ``` ## Deviations from Plan None — plan executed exactly as written. All 3 tasks completed; all artifacts produced; all `` satisfied. ## Confirmations (per `` section of PLAN) - Kermit tag = `"recipe"` (D-15) — set in `configureLogging()`. - `appModule` content: empty (D-14) — `val appModule = module { }`. - `App.kt` NOT modified (anti-pattern guard). - `MainViewController.kt` NOT modified (PITFALL #4 guard — Koin started outside). - `ContentView.swift` NOT modified (already wraps `MainViewControllerKt.MainViewController()`). ## Threat Mitigations Verified | Threat ID | Mitigation in delivered code | |-----------|------------------------------| | T-01-04-01 (Koin double-init iOS) | `KoinIosKt.doInitKoin()` is the only init call site on iOS; `MainViewController.kt` does not call `startKoin`. | | T-01-04-02 (Wasm init order) | webMain `main()` orders `configureLogging() → initKoin() → ComposeViewport { }`. | | T-01-04-03 (App.kt calling startKoin) | `App.kt` unchanged; verified no `startKoin` reference outside `Koin.kt`. | ## Verification gates - All three task `` grep blocks passed. - No build files modified → `tools/verify-no-version-literals.sh` and `tools/verify-shared-pure.sh` remain at exit 0. - Compile gates (`./gradlew build`, `:composeApp:jvmTest`) deferred to Plan 07 per the verification block in 01-04-PLAN.md. ## Commits - `cc5002d` — feat(01-04): add Koin + Kermit bootstrap commonMain + iOS bridge - `8cd608a` — feat(01-04): add Android MainApplication + manifest registration - `fd3e7e1` — feat(01-04): wire JVM + Wasm main + Swift iOSApp to bootstrap Koin + Kermit ## Self-Check: PASSED Files verified to exist on disk: - FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt - FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt - FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt - FOUND: composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt - FOUND: composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt - FOUND: composeApp/src/androidMain/AndroidManifest.xml (modified, contains `android:name=".MainApplication"`) - FOUND: composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt (modified) - FOUND: composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt (modified) - FOUND: iosApp/iosApp/iOSApp.swift (modified) Commits verified in `git log`: - FOUND: cc5002d - FOUND: 8cd608a - FOUND: fd3e7e1