# 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<T>` | `Lazy<T>` (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<T>` 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.
