# Koin Migration Implementation Plan (Annotations & K2 Compiler Plugin) This document outlines the meticulous, step-by-step strategy for migrating Meshtastic-Android from Hilt (Dagger) to **Koin with Annotations**. This approach leverages the new native **Koin Compiler Plugin (K2)** to automatically generate Koin DSL at compile time, providing a developer experience nearly identical to Hilt/Dagger but with pure, boilerplate-free KMP compatibility. We are targeting Koin 4.2.0-RC1+ and the Koin Compiler Plugin for maximum Compose Multiplatform support and optimal build performance. ## 1. Goal & Objectives - **Remove Hilt/Dagger completely** from the project. - **Adopt Koin Annotations** for declarative, compile-time verified DI using the native K2 Compiler Plugin. - **Eliminate Android*ViewModel Wrappers** by injecting KMP ViewModels (`@KoinViewModel`) directly. - **Improve Build Times** by replacing Dagger KAPT/KSP with the lightweight, native Koin Compiler Plugin. - **Maintain Incremental Progress** using the Strangler Fig Pattern. ## 2. Phase 1: Infrastructure Setup **Objective:** Add Koin Annotations and Koin Compiler Plugin to the build system. 1. **Add Dependencies** in `gradle/libs.versions.toml`: - Ensure versions are at least Koin `4.2.0-RC1` (or stable when available) and Koin Compiler Plugin. - Dependencies: `koin-core`, `koin-android`, `koin-annotations`, `koin-compose-viewmodel`. - Plugins: `io.insert-koin.compiler.plugin`. 2. **Configure Root Compiler Plugin** in `build.gradle.kts` (root or build-logic): - Ensure the plugin is available and applied in KMP modules (`alias(libs.plugins.koin.compiler)`). 3. **Setup Koin Application** in `MeshUtilApplication.kt`: - Initialize Koin with `startKoin { androidContext(this@MeshUtilApplication); modules(AppModule().module) }`. - *Note:* `.module` is an extension property automatically generated by the compiler plugin for classes annotated with `@Module`. - *Note:* In Koin 4.1+, standard native Context handling is unified, making explicit `androidContext` passing into KMP modules significantly simpler than in Koin 3.x. ## 3. Phase 2: Core Modules Migration (`core:*`) **Objective:** Replace Hilt modules with Koin Annotated modules. 1. **Annotate Classes**: - Replace `@Singleton` + `@Inject constructor` with just `@Single`. - Koin automatically binds implementations to their interfaces if it's the only interface implemented. - Standard constructor injection requires no explicit `@Inject` annotations—the compiler auto-detects constructors from the class-level scope annotation (`@Single`, `@Factory`, etc.). 2. **Define Koin Modules (`expect` / `actual` Pattern)**: - KMP Best Practice: In `commonMain`, declare an `expect val platformModule: Module`. - In each platform source set (e.g., `androidMain`, `iosMain`), implement this with `actual val platformModule: Module = module { includes(AndroidModule().module) }`. - Use `@Module` and `@ComponentScan("org.meshtastic.core.module")` on these platform-specific classes so the plugin builds the platform dependency graphs correctly. 3. **Bridge Hilt/Koin (Incremental Step)**: - If a Hilt class needs a Koin dependency, provide a temporary Hilt `@Provides` that fetches from `GlobalContext.get().get()`. 4. **`expect` / `actual` Class Injection**: - When you have an `expect class` that you want to inject, do *not* annotate the `expect` declaration. - Instead, annotate each platform's `actual class` with `@Single` or `@Factory`. The compiler plugin will automatically compile-time link the injected interface to the correct platform implementation. ## 4. Phase 3: Feature & ViewModel Migration [COMPLETED] **Objective:** Migrate ViewModels and eliminate Android-specific wrappers using latest mapping features. 1. **Migrate ViewModels**: - Replace `@HiltViewModel` with `@KoinViewModel`. - Move ViewModels to `commonMain` where applicable to share logic across targets. 2. **Update Compose Navigation**: - Replace `hiltViewModel()` with `koinViewModel()` in `app/navigation/`. - *Nitty-Gritty:* If using nested Jetpack Navigation graphs, leverage Koin 4.1's `koinNavViewModel()` to replicate Hilt's graph-scoped ViewModels securely. 3. **Compose Previews Integration (Experimental)**: - Replace dummy Hilt setups in `@Preview` with Koin's `KoinApplicationPreview` to inject dummy modules specifically for rendering Compose previews. 4. **Purge Wrappers**: - Delete `AndroidMetricsViewModel`, `AndroidRadioConfigViewModel`, etc. ## 5. Phase 4: Advanced Edge Cases (`@AssistedInject` & WorkManager) **Objective:** Address Dagger-specific advanced injection patterns. 1. **WorkManager & `@HiltWorker`**: - Add `io.insert-koin:koin-androidx-workmanager` to dependencies. - Replace `@HiltWorker` and `@AssistedInject` on Workers with `@KoinWorker`. - Initialize WorkManager factory in `MeshUtilApplication` via `WorkManagerFactory()`. 2. **`@AssistedInject` (Non-Worker classes)**: - Meshtastic heavily uses AssistedInject for Radio Interfaces (`NordicBleInterface`, `MockInterface`, etc.). - Replace `@AssistedInject` with Koin's `@Factory` on the class. - Replace `@Assisted` parameters in the constructor with `@InjectedParam`. - In Koin Annotations, when injecting this factory, you pass parameters dynamically: `val radio: RadioInterface = get { parametersOf(address) }`. 3. **Dagger Custom `@Qualifier`s**: - Project uses many custom qualifiers (e.g., `@UiDataStore`, `@MapDataStore`) for DataStore instances. - Replace these custom annotations with Koin's `@Named("UiDataStore")`. - Apply `@Named` to both the provided dependency (e.g., inside the `@Module` function) *and* the constructor parameter where it is injected. 4. **Compiler Plugin Multiplatform Benefit**: - By using the new `io.insert-koin.compiler.plugin`, we completely bypass the old KSP boilerplate. There is no need for `kspCommonMainMetadata` or complex KSP target wiring in KMP modules. ## 6. Phase 5: Testing & Final Cleanup **Objective:** Complete Hilt eradication and verify tests. 1. **Update Tests**: - Replace `@HiltAndroidTest` with Koin testing utilities. - Use `KoinTest` interface and `KoinTestRule` in your Android instrumented tests and Robolectric unit tests to supply mock modules. 2. **Remove Hilt Annotations**: - Delete `@HiltAndroidApp`, `@AndroidEntryPoint`, `@InstallIn`, etc. 3. **Clean Build Scripts**: - Remove Hilt plugins and dependencies from all `build.gradle.kts` and `libs.versions.toml`. 4. **Final Verification**: - Run `./gradlew clean assembleDebug test` to ensure successful compilation and structural integrity. ## 6. Migration Key mappings (Cheat Sheet) | Hilt/Dagger | Koin Annotations | | :--- | :--- | | `@Singleton class X @Inject constructor(...)` | `@Single class X(...)` | | `@Module` + `@InstallIn` | `@Module` + `@ComponentScan` | | `@Provides` | `@Single` or `@Factory` on a module function | | `@Binds` | Automatic (or `@Single` on implementation) | | `@HiltViewModel` | `@KoinViewModel` | | `hiltViewModel()` | `koinViewModel()` or `koinNavViewModel()` | | `Lazy` | `Lazy` (Native Kotlin) | | Dummy `@Preview` ViewModels | `KoinApplicationPreview { ... }` | ## 7. Troubleshooting & Lessons Learned (March 2026) ### Koin K2 Compiler Plugin Signature Collision During Phase 3, we discovered a bug in the Koin K2 Compiler Plugin (v0.3.0) where multiple `@Single` provider functions in the same module with identical JVM signatures (e.g., several `DataStore` providers taking `(Context, CoroutineScope)`) were incorrectly mapped to the same internal lambda. This caused `ClassCastException` at runtime (e.g., `LocalStats` being cast to `Preferences`). **Solution:** Split providers with identical signatures into separate `@Module` classes. This forces the compiler plugin to generate unique mapping classes, preventing the collision. ### Circular Dependencies in Koin 4.2.0 True circular dependencies (e.g., `Service -> InterfaceFactory -> Spec -> Factory -> Service`) can cause `StackOverflowError` during graph resolution even with `Lazy` injection if the `Lazy` is accessed too early (e.g., in a coroutine launched from `init`). **Solution:** Break cycles by passing dependencies as function parameters instead of constructor parameters where possible (e.g., passing `service` to `InterfaceSpec.createInterface(...)`). ### Robolectric Tests & KoinApplicationAlreadyStartedException When running Robolectric tests, `MeshUtilApplication` is recreated for each test. If `startKoin` is called in `onCreate` but not stopped, subsequent tests will fail with `org.koin.core.error.KoinApplicationAlreadyStartedException`. **Solution:** Explicitly call `org.koin.core.context.stopKoin()` in the application's `onTerminate` method, which is invoked by Robolectric during teardown. --- **Status:** **Fully Completed & Stable.** - Hilt completely removed. - Koin Annotations and K2 Compiler Plugin fully integrated. - All DataStore and Circular Dependency issues resolved. - App verified stable on device via Logcat audit.