refactor(di): adopt @KoinApplication with startKoin<T>() 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>
This commit is contained in:
James Rich 2026-04-15 17:52:59 -05:00 committed by GitHub
parent 0f900fe7d7
commit 8e5d99410c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 67 additions and 14 deletions

View file

@ -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<T>()` 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<AndroidKoinApp> {
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<T>()` (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`

View file

@ -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<AndroidKoinApp> {
androidContext(this@MeshUtilApplication)
workManagerFactory()
modules(AppKoinModule().module())
}
// Schedule periodic MeshLog cleanup

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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

View file

@ -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<T>() 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<AndroidKoinApp>()
try {
// No-op: reaching this point proves the typed bootstrap path did not
// throw and the generated application could be created.
} finally {
app.close()
}
}
}

View file

@ -29,11 +29,12 @@ class KoinConventionPlugin : Plugin<Project> {
// 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)
}