mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
122 lines
9 KiB
Markdown
122 lines
9 KiB
Markdown
# 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.
|