From 8e5d99410c0692d33e94449516bc372facd2c8da Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:52:59 -0500 Subject: [PATCH] refactor(di): adopt @KoinApplication with startKoin() compiler plugin API (#5152) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .skills/navigation-and-di/SKILL.md | 20 +++++++++++--- .../org/meshtastic/app/MeshUtilApplication.kt | 8 +++--- .../org/meshtastic/app/di/AndroidKoinApp.kt | 26 +++++++++++++++++++ .../meshtastic/app/di/KoinVerificationTest.kt | 16 ++++++++++++ .../src/main/kotlin/KoinConventionPlugin.kt | 11 ++++---- 5 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt diff --git a/.skills/navigation-and-di/SKILL.md b/.skills/navigation-and-di/SKILL.md index e92e2cfa3..c9d7336a6 100644 --- a/.skills/navigation-and-di/SKILL.md +++ b/.skills/navigation-and-di/SKILL.md @@ -12,15 +12,26 @@ This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Na 4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs. ### Anti-Patterns -- **A1 Module Compile Safety:** Do **not** enable A1 `compileSafety`. We rely on Koin's A3 full-graph validation (`startKoin` / `VerifyModule`) because of our decoupled Clean Architecture design (interfaces in one module, implemented in another). +- **A1 Module Compile Safety:** Do **not** enable `compileSafety`. It is a single boolean that enables A1 per-module checks — there is no separate A3 full-graph mode. Runtime graph verification is handled by `KoinVerificationTest` and `DesktopKoinTest` instead. - **Default Parameters:** Do **not** expect Koin to inject default parameters automatically. The K2 plugin's `skipDefaultValues = true` behavior skips parameters with default Kotlin values. ### Koin Startup Pattern (K2 Compiler Plugin) -The project uses the **K2 Compiler Plugin** (`koin-compiler-plugin`, not KSP). The correct canonical startup for this path is: +The project uses the **K2 Compiler Plugin** (`koin-compiler-plugin`, not KSP). The canonical startup uses the plugin's typed `startKoin()` stub, which the plugin transforms at compile time via IR: ```kotlin -startKoin { modules(AppKoinModule().module()) } +// Bootstrap class — separate from @Module, references the root module graph +@KoinApplication(modules = [AppKoinModule::class]) +object AndroidKoinApp + +// In Application.onCreate() +startKoin { + androidContext(this@MeshUtilApplication) + workManagerFactory() +} ``` -Do **not** use `@KoinApplication` — that annotation is part of the **KSP annotations path** (`koin-ksp-compiler`) and generates a `startKoin()` extension via KSP. It is incompatible with the K2 plugin approach. The two paths are mutually exclusive; the project has deliberately chosen K2 for compile-time wiring without KSP overhead. +- `@KoinApplication` goes on a **dedicated bootstrap object**, not on a `@Module` class. +- `startKoin()` (from `org.koin.plugin.module.dsl`) is a compiler plugin stub — if the plugin isn't applied, it throws `NotImplementedError`. +- `stopKoin()` uses the standard runtime API (`org.koin.core.context.stopKoin`). +- `compileSafety` must stay **disabled** — it enables A1 per-module checks that break our inverted-dependency architecture. There is no separate A3 full-graph flag. ## Navigation 3 @@ -39,6 +50,7 @@ Do **not** use `@KoinApplication` — that annotation is part of the **KSP annot ## Reference Anchors - **App Startup / Koin Bootstrap:** `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt` +- **DI Bootstrap Object:** `app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt` - **DI App Wiring:** `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` - **Shared Routes:** `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt` - **Desktop Nav Shell:** `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt` diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt index 34d4797cd..9228b6874 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt @@ -37,9 +37,8 @@ import kotlinx.coroutines.withTimeout import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.androidx.workmanager.koin.workManagerFactory -import org.koin.core.context.startKoin -import org.meshtastic.app.di.AppKoinModule -import org.meshtastic.app.di.module +import org.koin.plugin.module.dsl.startKoin +import org.meshtastic.app.di.AndroidKoinApp import org.meshtastic.core.common.ContextServices import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.repository.MeshPrefs @@ -64,10 +63,9 @@ open class MeshUtilApplication : super.onCreate() ContextServices.app = this - startKoin { + startKoin { androidContext(this@MeshUtilApplication) workManagerFactory() - modules(AppKoinModule().module()) } // Schedule periodic MeshLog cleanup diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt b/app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt new file mode 100644 index 000000000..04f0350c8 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.app.di + +import org.koin.core.annotation.KoinApplication + +/** + * Root Koin bootstrap for Android. The K2 compiler plugin uses this to discover the full module graph when + * [org.koin.plugin.module.dsl.startKoin] is called with this type parameter. + */ +@KoinApplication(modules = [AppKoinModule::class]) +object AndroidKoinApp diff --git a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt index 7b140cca8..30e1b6be7 100644 --- a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt +++ b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt @@ -25,6 +25,7 @@ import androidx.work.WorkerParameters import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import kotlinx.coroutines.CoroutineDispatcher +import org.koin.plugin.module.dsl.koinApplication import org.koin.test.verify.definition import org.koin.test.verify.injectedParameters import org.koin.test.verify.verify @@ -60,4 +61,19 @@ class KoinVerificationTest { ), ) } + + @Test + fun verifyTypedBootstrapLoadsModuleGraph() { + // koinApplication() is a K2 compiler plugin stub. If the plugin fails to + // transform it, the stub throws NotImplementedError at runtime. This test + // validates that the production bootstrap path is correctly transformed by + // successfully creating and closing the generated Koin application. + val app = koinApplication() + try { + // No-op: reaching this point proves the typed bootstrap path did not + // throw and the generated application could be created. + } finally { + app.close() + } + } } diff --git a/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt index 9b832ce16..b4f2acfbe 100644 --- a/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt @@ -29,11 +29,12 @@ class KoinConventionPlugin : Plugin { // Configure Koin K2 Compiler Plugin (0.4.0+) extensions.configure(KoinGradleExtension::class.java) { - // Meshtastic heavily utilizes dependency inversion across KMP modules. Koin's A1 - // per-module safety checks strictly enforce that all dependencies must be explicitly - // provided or included locally. This breaks decoupled Clean Architecture designs. - // We disable compile safety globally to properly rely on Koin's A3 full-graph - // validation which perfectly handles inverted dependencies at the composition root. + // Meshtastic uses dependency inversion across KMP modules — interfaces in + // commonMain, implementations wired at the composition root. Koin's compileSafety + // flag enables A1 per-module checks that treat every module as self-contained, + // which breaks this pattern. There is no separate flag for A3 full-graph + // validation. Until Koin exposes granular safety levels we keep this disabled; + // runtime graph verification is handled by KoinVerificationTest instead. compileSafety.set(false) }