docs: summarize KMP migration progress and architectural decisions (#4770)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-12 21:23:25 -05:00 committed by GitHub
parent bdfd7b9251
commit 84bb6d24e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 3554 additions and 189 deletions

22
docs/archive/README.md Normal file
View file

@ -0,0 +1,22 @@
# Archive
Historical and completed planning documents. Kept for git history and reference.
For current state, see [`docs/kmp-status.md`](../kmp-status.md).
For the forward-looking roadmap, see [`docs/roadmap.md`](../roadmap.md).
For decision records, see [`docs/decisions/`](../decisions/).
## Contents
| Document | Original Purpose | Status |
|---|---|---|
| `kmp-progress-review-2026.md` | Evidence-backed KMP re-baseline (729 lines) | Superseded by `kmp-status.md` |
| `kmp-progress-review-evidence.md` | Raw evidence appendix | Superseded by `kmp-status.md` |
| `kmp-migration.md` | Historical migration narrative | Superseded by `kmp-status.md` |
| `desktop-and-multi-target-roadmap.md` | Desktop roadmap + 41-item execution log | Superseded by `roadmap.md` |
| `kmp-adaptive-compose-evaluation.md` | JetBrains Material 3 Adaptive evaluation | All phases complete |
| `kmp-app-migration-assessment.md` | Expect/actual consolidation + app module assessment | All work complete |
| `ble-kmp-abstraction-plan.md` | BLE KMP abstraction execution plan | Complete |
| `ble-kmp-strategy.md` | BLE library comparison (Nordic vs KABLE) | Decision made; see `decisions/ble-strategy.md` |
| `koin-migration-plan.md` | Hilt → Koin step-by-step plan | Complete; see `decisions/koin-migration.md` |

View file

@ -0,0 +1,34 @@
# Phase 8: `core:ble` KMP Abstraction
## Objective
Migrate `core:ble` from an Android-only library (`meshtastic.android.library`) to a Kotlin Multiplatform library (`meshtastic.kmp.library`). The goal is to provide a unified, platform-agnostic Bluetooth Low Energy (BLE) interface for the rest of the application (e.g., `core:domain`, `core:data`), while explicitly supporting future Desktop and Web targets.
## Strategy: The "Nordic Hybrid" Abstraction
We will use an Interface-Driven (Dependency Injection) approach rather than relying directly on Nordic's KMM library in `commonMain` or using raw `expect`/`actual` for the entire BLE stack.
Nordic's [KMM-BLE-Library](https://github.com/NordicSemiconductor/Kotlin-BLE-Library) provides excellent, battle-tested Coroutine/Flow APIs for Android and iOS. However, it **does not support Desktop (JVM/Windows/Linux/macOS) or Web (Wasm/JS)**. If we expose Nordic's classes directly in `commonMain`, the project will fail to compile for Desktop/Web targets.
To resolve this, we will build a custom abstraction layer:
### 1. The Common Interfaces (`commonMain`)
Define pure Kotlin interfaces and data classes representing BLE operations. The rest of the app will only know about these interfaces.
* `BleScanner`: For discovering devices.
* `BleDevice`: Represents a remote peripheral.
* `BleConnectionManager`: Handles connect/disconnect, MTU negotiation, and characteristic read/write/subscribe operations.
* *Note: No Nordic dependencies will exist in `commonMain`.*
### 2. The Android & iOS Implementations (`androidMain` & `iosMain`)
These source sets will depend on the Nordic `KMM-BLE-Library`. We will write concrete implementations of our common interfaces (e.g., `NordicBleConnectionManager`) that delegate operations to Nordic's `CentralManager` and `Peripheral` classes.
### 3. The Future Implementations (`desktopMain` / `webMain`)
By keeping `commonMain` free of Nordic dependencies, we reserve the ability to implement our BLE interfaces using other libraries (like [Kable](https://github.com/JuulLabs/kable) or Web Bluetooth APIs) on unsupported platforms without rewriting the core application logic.
## Execution Plan
1. ✅ **Refactor Build Script:** Convert `core/ble/build.gradle.kts` to use the KMP plugin and define `commonMain` and `androidMain` source sets. Move Nordic dependencies to `androidMain`.
2. ✅ **Define Abstractions:** Create pure Kotlin interfaces (`BleScanner`, `BleConnection`, etc.) in `commonMain`.
3. ✅ **Implement Wrappers:** Move the existing Android-specific Nordic implementation into `androidMain` and adapt it to implement the new `commonMain` interfaces.
4. ✅ **Update DI:** Adjust the Hilt/DI modules in `app` or `androidMain` to bind the Android-specific Nordic wrappers to the common interfaces.
5. ✅ **Verify:** Ensure the Android app builds and tests pass, confirming the abstraction works correctly.
## Status: Completed
This phase was successfully executed. The Nordic SDK is now fully wrapped by common KMP interfaces (`BleDevice`, `BleScanner`, etc.). The DI modules have been relocated to the `app` module to accommodate Hilt limitations with KMP projects. All tests and integrations have been updated to use the new abstracted interfaces.

View file

@ -0,0 +1,111 @@
# `core:ble` KMP Strategy Analysis
> Date: 2026-03-10
>
> Context: Nordic responded to [our inquiry](https://github.com/NordicSemiconductor/Kotlin-BLE-Library/issues/183#issuecomment-4030710057) confirming KMP is on their roadmap but not yet available, and recommended KABLE for projects needing KMP now.
## Current State — Already Well-Architected
Our `core:ble` is **already one of the best-structured modules in the repo** for KMP:
| Layer | What exists | KMP-ready? |
|---|---|---|
| `commonMain` interfaces | `BleConnection`, `BleScanner`, `BleDevice`, `BleConnectionFactory`, `BluetoothRepository`, `BleConnectionState`, `BleService`, `BleRetry`, `MeshtasticBleConstants` | ✅ Pure Kotlin — zero platform imports |
| `androidMain` implementations | `AndroidBleConnection`, `AndroidBleScanner`, `AndroidBleDevice`, `AndroidBleConnectionFactory`, `AndroidBluetoothRepository`, `AndroidBleService` | ✅ Properly isolated |
| DI | `CoreBleModule` (commonMain), `CoreBleAndroidModule` (androidMain) | ✅ Clean split |
**The abstraction boundary is already drawn exactly where it needs to be.** No Nordic types leak into `commonMain`.
## The JVM Target Question
Adding `jvm()` to `core:ble` is **easy right now** — the `commonMain` has zero platform dependencies. The only blocker would be providing `jvmMain` implementations of the BLE interfaces, but for JVM (headless/desktop) we have two options:
### Option A: No-op / Stub JVM Implementation (Minimal, Unblocks CI Now)
Add `jvm()` and provide no-op or stub implementations in `jvmMain` (or don't — `commonMain` is just interfaces, it'll compile fine with no `jvmMain` source at all). Consumers on JVM would get `BleScanner`/`BleConnection` etc. from DI; a headless JVM app would simply not wire BLE into the graph.
**Effort: ~10 minutes. Unblocks JVM smoke compile immediately.**
### Option B: KABLE-backed JVM Implementation (Real Desktop BLE)
Replace or supplement the Nordic `androidMain` implementation with KABLE in `commonMain` or platform-specific source sets.
## Library Comparison
### Nordic Kotlin-BLE-Library (current: `2.0.0-alpha16`)
| Aspect | Status |
|---|---|
| Module structure | `core` and `client-core` are **pure JVM** (no Android dependencies). `client-android`, `environment-android` etc. are Android-only. |
| KMP status | **Not KMP yet.** `core` & `client-core` are JVM-only modules (not KMP multiplatform). No `iosMain`, no `commonMain` with `expect`/`actual`. |
| Roadmap | Nordic says: _"The library is intended to eventually be multiplatform on its own"_ but _"I don't have much KMP experience yet, we just started experimenting."_ |
| Our coupling | 5 Nordic imports across 6 `androidMain` files. All wrapped behind our `commonMain` interfaces. |
| Mocking | ✅ Has `client-android-mock`, `core-mock` modules — we use these in tests |
| Stability | Alpha (`2.0.0-alpha16`) — API still changing (recent breaking change in alpha16: `services()` emission) |
### KABLE (JuulLabs, current: `0.42.0`)
| Aspect | Status |
|---|---|
| KMP targets | ✅ Android, iOS, macOS, JVM, JavaScript, Wasm |
| API style | Coroutines/Flow-first. `Scanner`, `Peripheral`, `connect()`, `observe()`, `read()`, `write()` |
| JVM support | ✅ Uses Bluetooth on macOS/Linux/Windows via native bindings |
| Mocking | ❌ No mock module (Nordic's advantage) |
| Maturity | More mature than Nordic's KMP story, actively maintained |
| License | Apache 2.0 |
| Our coupling cost | Would need to rewrite 6 `androidMain` files (~400 lines total) |
## Recommended Strategy
### Phase 1: Add `jvm()` Target Now (No Library Change) ✅ COMPLETED
Since `commonMain` is already pure Kotlin interfaces, `jvm()` has been added to `core:ble/build.gradle.kts`. No JVM BLE implementation is needed — the interfaces compile fine and a headless JVM app simply wouldn't inject BLE bindings.
This unblocked `core:ble` in the JVM smoke compile. CI now validates `core:ble:compileKotlinJvm` on every PR.
### Phase 2: Evaluate Whether to Migrate to KABLE (Strategic Decision)
There are three paths, and the right one depends on project goals:
#### Path A: Stay on Nordic, Wait for Their KMP Support
- **Pro:** Zero migration work, we're already well-abstracted
- **Pro:** Nordic's mock modules are valuable for testing
- **Con:** Nordic says KMP is "intended" but has no timeline and "just started experimenting"
- **Con:** Nordic library is still alpha (API instability risk)
- **Risk:** Could be waiting 1+ years
#### Path B: Migrate to KABLE for `commonMain`, Keep Nordic as Optional Android Backend
- **Pro:** Real KMP BLE across all targets immediately
- **Pro:** KABLE is production-ready and actively maintained
- **Con:** ~400 lines of adapter code to rewrite
- **Con:** No built-in mock support (would need our own test doubles)
- **Con:** Two BLE library dependencies during transition
#### Path C: Dual-Backend Architecture (Best of Both Worlds)
Keep `commonMain` interfaces as-is. Add a `kableMain` or use KABLE in `commonMain` only for platforms that need it (JVM/iOS), keep Nordic on Android.
This is **overkill for now** but the architecture already supports it — our `BleConnection`/`BleScanner` interfaces would have multiple implementations selected via DI.
### Recommendation
**Phase 1 completed** (`jvm()` added, CI validates it).
For Phase 2: **Path A (stay on Nordic, wait)** is the pragmatic choice for now because:
1. Our abstraction layer is already clean — switching BLE backends later is a bounded, mechanical task
2. Nordic is actively developing (alpha16 released March 4, 2026 — 6 days ago)
3. We don't currently need real BLE on JVM/iOS
4. The mock modules are genuinely useful for testing
If Nordic hasn't shipped KMP by the time we're ready for iOS, revisit KABLE. The migration cost is predictable: ~6 files, ~400 lines, all in `androidMain``commonMain`.
## Potential Contribution to Nordic
Nordic is open to help. High-impact contributions we could make:
1. **File an issue or PR** showing how `core` and `client-core` could become `kotlin("multiplatform")` modules with `commonMain` + `jvmMain` source sets (they're pure JVM already — it's a build config change)
2. **Propose the `expect`/`actual` pattern** for `CentralManager` / `Peripheral` interfaces, showing how our wrapper demonstrates the abstraction boundary
3. **Share our `commonMain` interface design** as a reference for what a KMP-ready API surface looks like
This would accelerate their timeline and reduce our eventual migration friction.

View file

@ -0,0 +1,243 @@
# Desktop & Multi-Target Roadmap
> Date: 2026-03-11
>
> Desktop is the first non-Android target, but every decision here is designed to benefit **all future targets** (iOS, web, etc.). The guiding principle: solve problems in `commonMain` or behind shared interfaces — never in a target-specific way when it can be avoided.
## Current State
### What works today
| Layer | Status |
|---|---|
| Desktop scaffold | ✅ Compiles, runs, Navigation 3 shell with NavigationRail |
| Koin bootstrap | ✅ Full DI graph — stubs for all repository interfaces |
| Core KMP modules with `jvm()` | ✅ 16/16 (all core KMP modules) |
| Feature modules with `jvm()` | ✅ 6/6 — all feature modules compile on JVM |
| CI JVM smoke compile | ✅ 16 core + 6 feature modules + `desktop:test` |
| Repository stubs for non-Android | ✅ Full set in `desktop/src/main/kotlin/org/meshtastic/desktop/stub/` |
| Navigation 3 shell | ✅ Shared routes, NavigationRail, NavDisplay with placeholder screens |
| JetBrains lifecycle/nav3 forks | ✅ `org.jetbrains.androidx.lifecycle` + `org.jetbrains.androidx.navigation3` |
| Real settings feature screens | ✅ ~35 settings composables wired via `DesktopSettingsNavigation.kt` (all config + module screens) |
| Real node feature screens | ✅ Adaptive node list with real `NodeDetailContent`, TracerouteLog, NeighborInfoLog, HostMetricsLog |
| Real messaging feature screens | ✅ Adaptive contacts list with real `DesktopMessageContent` (non-paged message view with send) |
| Real connections screen | ✅ `DesktopConnectionsScreen` with TCP address entry, connection state display |
| Real TCP transport | ✅ Shared `StreamFrameCodec` + `TcpTransport` in `core:network`, used by both `app` and `desktop` |
| Mesh service controller | ✅ `DesktopMeshServiceController` — full `want_config` handshake, config/nodeinfo exchange |
| Remaining feature screens | ❌ Map, chart-based metrics (DeviceMetrics, etc.) |
| Remaining transport | ❌ Serial/USB, MQTT |
### Module JVM target inventory
**Core modules with `jvm()` target (16):**
`core:proto`, `core:common`, `core:model`, `core:repository`, `core:di`, `core:navigation`, `core:resources`, `core:datastore`, `core:database`, `core:domain`, `core:prefs`, `core:network`, `core:data`, `core:ble`, `core:service`, `core:ui`
**Core modules that are Android-only by design (3):**
`core:api` (AIDL), `core:barcode` (camera), `core:nfc` (NFC hardware)
**Feature modules (6) — all have `jvm()` target and compile on JVM:**
`feature:intro`, `feature:messaging`, `feature:map`, `feature:node`, `feature:settings`, `feature:firmware`
**Modules with `jvmMain` source sets (hand-written actuals):**
`core:common` (4 files), `core:model` (via `jvmAndroidMain`, 3 files), `core:network` (via `jvmAndroidMain`, 1 file — `TcpTransport.kt`), `core:repository` (1 file — `Location.kt`), `core:ui` (6 files — QR, clipboard, HTML, platform utils, time tick, dynamic color)
**Desktop feature wiring:**
`feature:settings` — fully wired with ~35 real composables via `DesktopSettingsNavigation.kt`, including 5 desktop-specific config screens (Device, Position, Network, Security, ExternalNotification). Other features remain placeholder.
---
## KMP Gaps — Resolved
These were pre-existing issues where `commonMain` code used symbols only available on Android. The JVM target surfaced them during Phase 1; all have been fixed.
### `feature:node` ✅ Fixed
- `formatUptime()` moved from `core:model/androidMain``commonMain` (pure `kotlin.time` — no platform deps)
- Material 3 Expressive APIs (`ExperimentalMaterial3ExpressiveApi`, `titleMediumEmphasized`, `IconButtonDefaults.mediumIconSize`, `shapes` param) replaced with standard Material 3 equivalents
- `androidMain/DateTimeUtils.kt` renamed to `AndroidDateTimeUtils.kt` to avoid JVM class name collision
### `feature:settings` ✅ Fixed
- Material 3 dependency wiring corrected (CMP `compose.material3` in commonMain)
**Fix pattern applied:** When `commonMain` code references APIs not in Compose Multiplatform, use the standard Material 3 equivalent. Don't create expect/actual wrappers unless the behavior genuinely differs by platform.
---
## Phased Roadmap
### Phase 0 — No-op Stubs for Repository Interfaces (target-agnostic foundation)
**Goal:** Let any non-Android target bootstrap a full Koin DI graph without crashing.
**Approach:** Create a `NoopStubs.kt` file in `desktop/` that provides no-op/empty implementations of every repository interface the graph requires. These are explicitly "does nothing" implementations — they return empty flows, no-op on mutations, and log warnings on write calls. This unblocks DI graph assembly for desktop AND establishes the stub pattern future targets will reuse.
**Why target-agnostic:** When iOS arrives, it will need the same stubs initially. The interfaces are all in `commonMain` already, so the stub pattern is inherently shared. Once real implementations exist (e.g., serial transport for desktop, CoreBluetooth for iOS), they replace the stubs per-target.
**Interfaces to stub (priority order):**
| Interface | Module | Notes |
|---|---|---|
| `ServiceRepository` | `core:repository` | Connection state, mesh packets, errors |
| `NodeRepository` | `core:repository` | Node DB, our node info |
| `RadioConfigRepository` | `core:repository` | Channel/config flows |
| `RadioInterfaceService` | `core:repository` | Raw radio bytes |
| `RadioController` | `core:model` | High-level radio commands |
| `PacketRepository` | `core:repository` | Message/packet queries |
| `MeshLogRepository` | `core:repository` | Log storage |
| `MeshServiceNotifications` | `core:repository` | Notifications (no-op on desktop) |
| `PacketHandler` | `core:repository` | Packet dispatch |
| `CommandSender` | `core:repository` | Command dispatch |
| `AlertManager` | `core:ui` | Alert dialog state |
| Preference interfaces | `core:repository` | `UiPrefs`, `MapPrefs`, `MeshPrefs`, etc. |
### Phase 1 — Add `jvm()` Target to Feature Modules ✅ COMPLETE
**Goal:** Feature modules compile on JVM, unblocking desktop (and future JVM-based targets) from using shared ViewModels and UI.
**Result:** All 6 feature modules have `jvm()` target and compile clean on JVM. KMP gaps discovered during this phase (Material 3 Expressive APIs, `formatUptime` placement) have been resolved.
**CI update:** All 6 feature module `:compileKotlinJvm` tasks added to the JVM smoke compile step.
### Phase 2 — Desktop Koin Graph Assembly
**Goal:** Desktop boots with a complete Koin graph — stubs for all platform services, real implementations where possible (database, datastore, network).
**Approach:** Create `desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` that mirrors `AppKoinModule` but uses:
- No-op stubs for radio/BLE/notifications
- Real Room KMP database (already has JVM constructor)
- Real DataStore preferences (already KMP)
- Real Ktor HTTP client (already KMP in `core:network`)
- Real firmware release repository (network + database)
This pattern directly transfers to iOS: replace `DesktopKoinModule` with `IosKoinModule`, swap stubs for CoreBluetooth-backed implementations.
### Phase 3 — Shared Navigation Shell 🔄 IN PROGRESS
**Goal:** Desktop shows a real multi-screen app with navigation, not a smoke report.
**Completed:**
- ✅ Switched Navigation 3 + lifecycle artifacts to JetBrains multiplatform forks (`org.jetbrains.androidx.navigation3` `1.1.0-alpha03`, `org.jetbrains.androidx.lifecycle` `2.10.0-alpha08`)
- ✅ Desktop app shell with `NavigationRail` for top-level destinations (Conversations, Nodes, Map, Settings, Connections)
- ✅ `NavDisplay` + `entryProvider` pattern matching the Android app's nav graph shape
- ✅ `SavedStateConfiguration` with polymorphic `SerializersModule` for non-Android NavKey serialization
- ✅ Shared routes from `core:navigation` used for both Android and Desktop navigation
- ✅ Placeholder screens for all top-level destinations
- ✅ **`feature:settings` wired with real composables** — ~30 screens including DeviceConfiguration, ModuleConfiguration, Administration, CleanNodeDatabase, FilterSettings, radio config routes (User, Channels, Power, Display, LoRa, Bluetooth), and module config routes (MQTT, Serial, StoreForward, RangeTest, Telemetry, CannedMessage, Audio, RemoteHardware, NeighborInfo, AmbientLighting, DetectionSensor, Paxcounter, StatusMessage, TrafficManagement, TAK)
- ✅ Desktop-specific top-level settings screen (`DesktopSettingsScreen.kt`) replacing Android-only `SettingsScreen`
**Remaining:**
- ~~Wire real feature composables from `feature:node`, `feature:messaging`, and `feature:map` into the desktop nav graph~~ → node and messaging done; map still placeholder
- ~~Some settings config sub-screens still use placeholders (Device Config, Position, Network, Security, ExtNotification, Debug, About)~~ → 5 config screens replaced with real desktop implementations; Debug and About remain placeholders
- Platform-specific screens (map, BLE scan) show "not available" placeholders
- Evaluate sidebar/tab hybrid for secondary navigation within features
### Phase 4 — Real Transport Layer 🔄 IN PROGRESS
**Goal:** Desktop can actually talk to a Meshtastic radio.
**Completed:**
- ✅ `DesktopRadioInterfaceService` — TCP socket transport with auto-reconnect, heartbeat, and backoff retry
- ✅ `DesktopMeshServiceController` — orchestrates the full `want_config` handshake (config → channels → nodeinfo exchange)
- ✅ `DesktopConnectionsScreen` — TCP address entry, service-level connection state display, recent addresses
- ✅ Transport state architecture — transport layer (`RadioInterfaceService`) reports binary connected/disconnected; service layer (`ServiceRepository`) manages Connecting state during handshake
**Transports (in priority order):**
| Transport | Platform | Library | Status |
|---|---|---|---|
| TCP | Desktop (JVM) | Ktor/Okio | ✅ Implemented |
| Serial/USB | Desktop (JVM) | jSerialComm | ❌ Not started |
| MQTT | All (KMP) | Ktor/MQTT | ❌ Not started |
| BLE | iOS | Kable/CoreBluetooth | ❌ Not started |
| BLE | Desktop | Kable (JVM) | ❌ Not started |
**Architecture:** The `RadioInterfaceService` contract in `core:repository` already defines the transport abstraction. Each transport is an implementation of that interface, registered via Koin. Desktop initially gets serial + TCP. iOS gets BLE.
### Phase 5 — Feature Parity Roadmap
| Feature | Desktop | iOS | Web |
|---|---|---|---|
| Node list | Phase 3 | Phase 3 | Later |
| Messaging | Phase 3 | Phase 3 | Later |
| Settings | Phase 3 | Phase 3 | Later |
| Map | Phase 4+ (MapLibre) | Phase 4+ (MapKit) | Later |
| Firmware update | Phase 5+ | Phase 5+ | N/A |
| BLE scanning | Phase 5+ (Kable) | Phase 3 (CoreBluetooth) | N/A |
| NFC/Barcode | N/A | Later | N/A |
---
## Cross-Target Design Principles
1. **Solve in `commonMain` first.** If logic doesn't need platform APIs, it belongs in `commonMain`. Period.
2. **Interfaces in `commonMain`, implementations per-target.** The repository pattern is already established — extend it.
3. **Stubs are a valid first implementation.** Every target starts with no-op stubs, then graduates to real implementations. This is intentional, not lazy.
4. **Feature modules stay target-agnostic in `commonMain`.** Android-specific UI goes in `androidMain`, desktop-specific UI goes in `jvmMain`, iOS-specific UI goes in `iosMain`.
5. **Transport is a pluggable adapter.** BLE, serial, TCP, MQTT are all implementations of the same radio interface contract.
6. **CI validates every target.** If a module declares `jvm()`, CI compiles it on JVM. No exceptions.
---
## Execution Status (updated 2026-03-11)
1. ✅ Create this roadmap document
2. ✅ Create no-op repository stubs in `desktop/stub/NoopStubs.kt` (all 30+ interfaces)
3. ✅ Create desktop Koin module in `desktop/di/DesktopKoinModule.kt`
4. ✅ Add `jvm()` to all 6 feature modules — **6/6 compile clean on JVM**
5. ✅ Update CI to include all feature module JVM smoke compile (6 modules)
6. ✅ Update docs: `AGENTS.md`, `.github/copilot-instructions.md`, `docs/agent-playbooks/task-playbooks.md`
7. ✅ Fix KMP debt in `feature:node` (Material 3 Expressive → standard M3, `formatUptime` → commonMain)
8. ✅ Fix KMP debt in `feature:settings` (dependency wiring)
9. ✅ Move `ConnectionsViewModel` to `core:ui` commonMain
10. ✅ Split `UIViewModel` into shared `BaseUIViewModel` + Android adapter
11. ✅ Switch Navigation 3 to JetBrains fork (`org.jetbrains.androidx.navigation3:navigation3-ui:1.1.0-alpha03`)
12. ✅ Switch lifecycle-runtime-compose and lifecycle-viewmodel-compose to JetBrains forks (`org.jetbrains.androidx.lifecycle:2.10.0-alpha08`)
13. ✅ Implement desktop Navigation 3 shell with `NavigationRail` + `NavDisplay` + placeholder screens
14. ✅ Wire `feature:settings` composables into desktop nav graph (~30 real screens)
15. ✅ Create desktop-specific `DesktopSettingsScreen` (replaces Android-only `SettingsScreen`)
16. ✅ Delete passthrough Android ViewModel wrappers (11 wrappers removed)
17. ✅ Migrate `feature:node` UI components from `androidMain``commonMain`
18. ✅ Migrate `feature:settings` UI components from `androidMain``commonMain`
19. ✅ Wire `feature:node` composables into the desktop nav graph (real `DesktopNodeListScreen` with shared `NodeListViewModel`, `NodeItem`, `NodeFilterTextField`)
20. ✅ Wire `feature:messaging` composables into the desktop nav graph (real `DesktopContactsScreen` with shared `ContactsViewModel`)
21. ✅ Add `feature:node`, `feature:messaging`, `feature:map` module dependencies to `desktop/build.gradle.kts`
22. ✅ Add JetBrains Material 3 Adaptive (`1.3.0-alpha05`) to version catalog and desktop module — see [`docs/kmp-adaptive-compose-evaluation.md`](./kmp-adaptive-compose-evaluation.md)
23. ✅ Create `DesktopAdaptiveContactsScreen` using `ListDetailPaneScaffold` (contacts list + message detail placeholder)
24. ✅ Create `DesktopAdaptiveNodeListScreen` using `ListDetailPaneScaffold` (node list + node detail placeholder, context menu)
25. ✅ Provide Ktor `HttpClient` (Java engine) in desktop Koin module — fixes `ApiServiceImpl``DeviceHardwareRemoteDataSource``IsOtaCapableUseCase``SettingsViewModel` injection chain
26. ✅ Wire real `NodeDetailContent` from commonMain into adaptive node list detail pane (replacing placeholder)
27. ✅ Move `ContactItem.kt` from `feature:messaging/androidMain``commonMain` (pure M3, no Android deps)
28. ✅ Extract `MetricLogComponents.kt` (shared `MetricLogItem`/`DeleteItem`) and move `TracerouteLog`, `NeighborInfoLog`, `TimeFrameSelector`, `HardwareModelExtensions` to commonMain
29. ✅ Wire TracerouteLog, NeighborInfoLog, HostMetricsLog as real screens in `DesktopNodeNavigation.kt` (replacing placeholders) with `MetricsViewModel` registered in desktop Koin module
30. ✅ Move `MessageBubble.kt` from `feature:messaging/androidMain``commonMain` (pure Compose, zero Android deps, made public)
31. ✅ Build `DesktopMessageContent` composable — non-paged message list with send input for contacts detail pane (replaces placeholder)
32. ✅ Add `getMessagesFlow()` to `MessageViewModel` — non-paged `Flow<List<Message>>` for desktop (avoids paging-compose dependency)
33. ✅ Implement `DesktopRadioInterfaceService` — TCP socket transport with auto-reconnect, heartbeat, and configurable backoff retry
34. ✅ Implement `DesktopMeshServiceController` — mesh service lifecycle orchestrator wiring `want_config` handshake chain (config → channels → nodeinfo)
35. ✅ Create `DesktopConnectionsScreen` — TCP address entry UI with service-level connection state display and recent address history
36. ✅ Fix transport state architecture — removed transport-level `Connecting` emission that blocked `want_config` handshake; transport now reports binary connected/disconnected, service layer owns the Connecting state during config exchange
37. ✅ Create 5 desktop-specific config screens replacing placeholders: `DesktopDeviceConfigScreen` (role, rebroadcast, timezone via JVM `ZoneId`), `DesktopPositionConfigScreen` (fixed position, GPS, position flags — omits Android Location), `DesktopNetworkConfigScreen` (WiFi, Ethernet, IPv4 — omits QR/NFC), `DesktopSecurityConfigScreen` (keys, admin, key regeneration via JVM `SecureRandom` — omits file export), `DesktopExternalNotificationConfigScreen` (GPIO, ringtone — omits MediaPlayer/file import)
38. ✅ **Transport Deduplication:** Extracted `StreamFrameCodec` (commonMain) and `TcpTransport` (jvmAndroidMain) into `core:network` — eliminates ~450 lines of duplicated framing/TCP code between `app` and `desktop`. `StreamInterface` and `TCPInterface` in `app` now delegate to shared codec/transport. `DesktopRadioInterfaceService` reduced from 455 → 178 lines. Added `StreamFrameCodecTest` in `core:network/commonTest`.
39. ✅ **EmojiPickerDialog — unified commonMain implementation:** Replaced the `expect`/`actual` split with a single fully-featured emoji picker in `core:ui/commonMain`. Features: 9 category tabs with bidirectional scroll-tab sync, keyword search, recently-used tracking (persisted via `EmojiPickerViewModel`/`CustomEmojiPrefs`), Fitzpatrick skin-tone selector, and ~1000+ emoji catalog with `EmojiData.kt`. Deleted Android `EmojiPicker.kt` (AndroidView wrapper), `CustomRecentEmojiProvider.kt`, and JVM `EmojiPickerDialog.kt` (flat grid). Removed `androidx-emoji2-emojipicker` and `guava` dependencies from `core:ui`.
40. ✅ **Messaging component migration:** Moved `MessageActions.kt`, `MessageActionsBottomSheet.kt`, `Reaction.kt` (minus previews), `DeliveryInfoDialog.kt` from `feature:messaging/androidMain``commonMain`. Extracted `MessageStatusIcon` from `MessageItem.kt` into shared `MessageStatusIcon.kt`. Removed `ExperimentalMaterial3ExpressiveApi` (Android-only). Preview functions remain in `androidMain/ReactionPreviews.kt`.
41. ✅ **PositionLog table migration:** Extracted `PositionLogHeader`, `PositionItem`, `PositionList` composables from `feature:node/androidMain` into shared `PositionLogComponents.kt` in `commonMain`. Android `PositionLogScreen` with CSV export stays in `androidMain`.
### Next: Connections UI, chart migration, remaining screens, and serial transport
Desktop now has:
- **TCP connectivity** with full `want_config` handshake and config exchange
- **Shared transport layer**`StreamFrameCodec` and `TcpTransport` in `core:network` used by both `app` and `desktop`
- **Shared messaging components**`MessageActions`, `ReactionRow`, `ReactionDialog`, `MessageStatusIcon`, `DeliveryInfo` all in commonMain
- **Shared position log**`PositionLogHeader`, `PositionItem`, `PositionList` in commonMain
- Adaptive list-detail screens for **nodes** (with real `NodeDetailContent`) and **contacts** (with real `DesktopMessageContent`)
- Real screens for **TracerouteLog**, **NeighborInfoLog**, **HostMetricsLog** metrics
- ~35 real **settings** screens (all config + module routes — only Debug Panel and About remain placeholder)
Next priorities:
- **Connections UI Unification:** Create `feature:connections` to merge the fragmented Android and Desktop connection screens, abstracting discovery mechanisms (BLE, USB, TCP) behind a shared interface.
- Evaluate KMP charting replacement for Vico (DeviceMetrics, EnvironmentMetrics, SignalMetrics, PowerMetrics, PaxMetrics)
- Wire serial/USB transport for direct radio connection on Desktop
- Wire MQTT transport for cloud relay operation
- **Hardware Abstraction:** Abstract `core:barcode` and `core:nfc` into `commonMain` interfaces with `androidMain` implementations.
- **iOS CI:** Turn on iOS compilation (`iosArm64()`, `iosSimulatorArm64()`) in the GitHub Actions CI pipeline to ensure the shared codebase remains LLVM-compatible.
- **Dependency Tracking:** Track stable releases for currently required alpha/RC dependencies (Compose Multiplatform `1.11.0-alpha03` for Adaptive layouts, Koin `4.2.0-RC1` for K2 plugin). Do not downgrade these prematurely as they enable critical KMP functionality.

View file

@ -0,0 +1,174 @@
# KMP Material 3 Adaptive Compose — Evaluation
> Date: 2026-03-10
>
> This evaluation assesses the availability and readiness of Compose Material 3 Adaptive libraries for Kotlin Multiplatform, specifically for enabling shared list-detail layouts (nodes, messaging) across Android and Desktop.
## Executive Summary
**Material 3 Adaptive is available as a multiplatform library** via JetBrains forks, with desktop and iOS targets. Version `1.3.0-alpha05` is built against the exact same CMP and Navigation 3 versions the project already uses. This unblocks moving `ListDetailPaneScaffold`-based screens into `commonMain` and wiring real adaptive layouts on desktop — no more placeholder screens for nodes and messaging.
## Current State in the Project
### What the project uses today
| API | File | Source Set | Maven Coordinates |
|---|---|---|---|
| `ListDetailPaneScaffold` | `app/.../AdaptiveNodeListScreen.kt` | `app` (Android-only) | `androidx.compose.material3.adaptive:adaptive-layout:1.2.0` |
| `ListDetailPaneScaffold` | `feature/messaging/.../AdaptiveContactsScreen.kt` | `androidMain` | `androidx.compose.material3.adaptive:adaptive-layout:1.2.0` |
| `NavigationSuiteScaffold` | `app/.../Main.kt` | `app` (Android-only) | `androidx.compose.material3:material3-adaptive-navigation-suite` (BOM) |
| `currentWindowAdaptiveInfo` | `app/.../Main.kt` | `app` (Android-only) | `androidx.compose.material3.adaptive:adaptive:1.2.0` |
### Imports used across the codebase
```
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
```
### Where the dependencies are declared
- `gradle/libs.versions.toml`: `androidxComposeMaterial3Adaptive = "1.2.0"` → AndroidX (Android-only)
- `app/build.gradle.kts`: `androidMain` only
- `feature/messaging/build.gradle.kts`: `androidMain` only
## JetBrains Multiplatform Adaptive Artifacts
JetBrains publishes multiplatform forks of Material 3 Adaptive with full target coverage:
### Artifact inventory
| JetBrains Artifact | AndroidX Equivalent | Desktop | iOS | Status |
|---|---|---|---|---|
| `org.jetbrains.compose.material3.adaptive:adaptive` | `androidx.compose.material3.adaptive:adaptive` | ✅ | ✅ | Published on Maven Central |
| `org.jetbrains.compose.material3.adaptive:adaptive-layout` | `androidx.compose.material3.adaptive:adaptive-layout` | ✅ | ✅ | Published on Maven Central |
| `org.jetbrains.compose.material3.adaptive:adaptive-navigation` | `androidx.compose.material3.adaptive:adaptive-navigation` | ✅ | ✅ | Published on Maven Central |
| `org.jetbrains.compose.material3.adaptive:adaptive-navigation3` | _(new, no AndroidX equivalent)_ | ✅ | ✅ | Published on Maven Central (1.3.0+ only) |
| `org.jetbrains.compose.material3:material3-adaptive-navigation-suite` | `androidx.compose.material3:material3-adaptive-navigation-suite` | ✅ | ✅ | Bundled with CMP `material3` at `composeMaterial3Version` |
### Package names are identical
The JetBrains forks use the same `androidx.compose.material3.adaptive.*` package names as AndroidX. **No import changes are needed** — only the Maven coordinates in `build.gradle.kts` change.
### Version compatibility matrix
| JB Adaptive Version | CMP Version | Navigation 3 | Kotlin | Match? |
|---|---|---|---|---|
| **`1.3.0-alpha05`** | **`1.11.0-alpha03`** | **`1.1.0-alpha03`** | `2.2.20` | ✅ **Exact match** on CMP + Nav3 |
| `1.2.0` | `1.9.0` | — | `2.1.21` | ❌ Too old for this project |
| `1.1.2` | `1.8.x` | — | — | ❌ Too old |
**`1.3.0-alpha05` is the correct version** — it is built against `foundation:1.11.0-alpha03` and `navigation3-ui:1.1.0-alpha03`, both of which are the exact versions the project uses today.
### `adaptive-navigation3` — new Navigation 3 integration
The `adaptive-navigation3` artifact is a brand-new addition at `1.3.0`. It provides Navigation 3-aware adaptive scaffolding. Its POM shows dependencies on:
- `navigation3-ui-desktop:1.1.0-alpha03`
- `navigationevent-compose-desktop:1.0.1`
This could eventually enable deeper Nav3 + adaptive integration (e.g., `ListDetailPaneScaffold` directly managing Nav3 back stacks), but it's not required for the initial migration.
## What This Enables
### Immediate opportunity: shared `ListDetailPaneScaffold`
The `ListDetailPaneScaffold` and its navigator can move into `commonMain` code. This directly enables:
1. **`AdaptiveNodeListScreen`** — currently in `app` (Android-only) — can be restructured so the scaffold pattern works cross-platform
2. **`AdaptiveContactsScreen`** — currently in `feature:messaging/androidMain` — same opportunity
3. **Desktop gets real list-detail layouts** instead of placeholder text
### Remaining Android-only blockers per file
Even with adaptive layouts available in `commonMain`, each file has additional Android-specific code that must be handled separately:
| File | Android-Only APIs Used | Migration Strategy |
|---|---|---|
| `AdaptiveNodeListScreen.kt` | `BackHandler`, `LocalFocusManager` | `BackHandler``expect/actual`; `LocalFocusManager` is already in CMP |
| `AdaptiveContactsScreen.kt` | `BackHandler` (same pattern) | Same as above |
| `NodeListScreen.kt` | `ExperimentalMaterial3ExpressiveApi`, `animateFloatingActionButton`, `LocalContext`, `showToast` | Expressive APIs → standard M3; toast → platform callback |
| `NodeDetailScreen.kt` | `android.Manifest`, `Intent`, `ActivityResultContracts`, `tooling.preview` | Heavy Android — keep in `androidMain`, create desktop variant |
| `Main.kt` (app) | `currentWindowAdaptiveInfo`, `NavigationSuiteScaffold` | App-only, desktop already uses `NavigationRail` — no migration needed |
### `NavigationSuiteScaffold` in desktop
The desktop already uses `NavigationRail` directly (in `DesktopMainScreen.kt`). The `NavigationSuiteScaffold` from the main `material3` group is already available multiplatform via `compose.material3AdaptiveNavigationSuite` in the CMP DSL (`composeMaterial3Version = "1.9.0"`), but it's not needed — the desktop's `NavigationRail` is a deliberate design choice that works better for desktop form factors.
## Risk Assessment
| Factor | Assessment |
|---|---|
| Library stability | Alpha, but same stability tier as CMP `1.11.0-alpha03` and Nav3 `1.1.0-alpha03` already in use |
| API surface stability | `ListDetailPaneScaffold` API is stable in practice (widely adopted since AndroidX `1.0.0`) |
| Build pipeline alignment | `1.3.0-alpha05` is produced by the same JetBrains compose-multiplatform build that produces CMP `1.11.0-alpha03` |
| Breaking change risk | Low — API surface matches AndroidX; only coordinates change |
| Dependency policy alignment | Follows project rule: "alpha only behind hard abstraction seams" (adaptive is behind feature module boundaries) |
## Recommended Approach
### Phase 1 — Add JetBrains adaptive dependencies ✅ DONE
Added to `gradle/libs.versions.toml`:
```toml
jetbrains-adaptive = "1.3.0-alpha05"
jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" }
jetbrains-compose-material3-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "jetbrains-adaptive" }
jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "jetbrains-adaptive" }
```
Added to `desktop/build.gradle.kts`:
```kotlin
implementation(libs.jetbrains.compose.material3.adaptive)
implementation(libs.jetbrains.compose.material3.adaptive.layout)
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
```
Desktop compile verified: `./gradlew :desktop:compileKotlin`**BUILD SUCCESSFUL**.
### Phase 2 — Desktop adaptive contacts screen ✅ DONE
1. Moved `adaptive`, `adaptive-layout`, `adaptive-navigation` dependencies from `androidMain.dependencies``commonMain.dependencies` in `feature:messaging/build.gradle.kts` (using JetBrains coordinates, replacing AndroidX adaptive)
2. Created `desktop/.../DesktopAdaptiveContactsScreen.kt` using `ListDetailPaneScaffold` with:
- List pane: shared `ContactItem` composable with `isActive` highlighting on selected contact
- Detail pane: real `DesktopMessageContent` — non-paged message list with send input using shared `MessageViewModel`
3. Wired into `DesktopMessagingNavigation.kt` for `ContactsRoutes.ContactsGraph` and `ContactsRoutes.Contacts`
4. Verified: `./gradlew :desktop:compileKotlin :feature:messaging:compileKotlinJvm :app:compileFdroidDebugKotlin` — **BUILD SUCCESSFUL**
### Phase 3 — Desktop adaptive node list screen ✅ DONE
1. Added JetBrains adaptive dependencies to `feature:node/build.gradle.kts` `commonMain.dependencies`
2. Created `desktop/.../DesktopAdaptiveNodeListScreen.kt` using `ListDetailPaneScaffold` with:
- List pane: shared `NodeItem`, `NodeFilterTextField`, `MainAppBar` composables; context menu for favorite/ignore/mute/remove; `isActive` highlighting
- Detail pane: real `NodeDetailContent` from commonMain — shared `NodeDetailList` with identity, device actions, position, hardware, notes, admin sections
3. Wired into `DesktopNodeNavigation.kt` for `NodesRoutes.NodesGraph` and `NodesRoutes.Nodes`
4. Metrics log screens (TracerouteLog, NeighborInfoLog, HostMetricsLog) wired as real screens with `MetricsViewModel` (replacing placeholders)
5. Verified: `./gradlew :desktop:compileKotlin :feature:node:compileKotlinJvm :app:compileFdroidDebugKotlin` — **BUILD SUCCESSFUL**
### Phase 4 — Optional: evaluate `adaptive-navigation3`
The new `adaptive-navigation3` artifact may offer cleaner Nav3 integration for list-detail patterns. Evaluate once the basic adaptive migration is stable.
## Decision
**Proceed with JetBrains adaptive `1.3.0-alpha05`.**
The version alignment is perfect, the risk profile matches what the project already accepts for CMP/Nav3/lifecycle, and the payoff is significant: shared list-detail layouts for nodes and messaging across Android and Desktop.
## References
- Maven Central: [`org.jetbrains.compose.material3.adaptive:adaptive`](https://repo1.maven.org/maven2/org/jetbrains/compose/material3/adaptive/adaptive/)
- Maven Central: [`adaptive-navigation3`](https://repo1.maven.org/maven2/org/jetbrains/compose/material3/adaptive/adaptive-navigation3/)
- AndroidX source: [`ListDetailPaneScaffold.kt` in `commonMain`](https://github.com/androidx/androidx/blob/main/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt)
- Current project dependency: `androidxComposeMaterial3Adaptive = "1.2.0"` in `gradle/libs.versions.toml`

View file

@ -0,0 +1,127 @@
# KMP Migration Assessment — App Module & Expect/Actual Evaluation
> Date: 2026-03-10
## Summary of Changes Made
### Expect/Actual Consolidation (Completed)
| Expect/Actual | Resolution | Rationale |
|---|---|---|
| `Base64Factory` | ✅ **Replaced** with pure `commonMain` using `kotlin.io.encoding.Base64` | Both Android/JVM used `java.util.Base64` — Kotlin stdlib provides a cross-platform equivalent |
| `isDebug` | ✅ **Replaced** with `commonMain` constant `false` | Both actuals returned `false`; runtime debug detection uses `BuildConfigProvider.isDebug` via DI |
| `NumberFormatter` | ✅ **Replaced** with pure Kotlin `commonMain` implementation | Both actuals used identical `String.format(Locale.ROOT, ...)` — pure math-based formatting works everywhere |
| `UrlUtils` | ✅ **Replaced** with pure Kotlin `commonMain` RFC 3986 encoder | Both actuals used `URLEncoder.encode` — simple byte-level encoding is trivially portable |
| `SfppHasher` | ✅ **Consolidated** into `jvmAndroidMain` intermediate source set | Byte-for-byte identical implementations using `java.security.MessageDigest` |
| `platformRandomBytes` | ✅ **Consolidated** into `jvmAndroidMain` intermediate source set | Byte-for-byte identical implementations using `java.security.SecureRandom` |
| `getShortDateTime` | ✅ **Consolidated** into `jvmAndroidMain` intermediate source set | Functionally identical `java.text.DateFormat` usage |
### Expect/Actual Retained (Genuinely Platform-Specific)
| Expect/Actual | Why It Must Remain |
|---|---|
| `BuildUtils` (isEmulator, sdkInt) | Android uses `Build.FINGERPRINT`/`Build.VERSION.SDK_INT`; JVM stubs return defaults |
| `CommonUri` | Android wraps `android.net.Uri`; JVM wraps `java.net.URI` — different parsing semantics |
| `CommonUri.toPlatformUri()` | Returns platform-native URI type for interop |
| `Parcelable` abstractions (6 declarations) | AIDL/Android Parcel is a fundamentally Android-only concept |
| `Location` | Android wraps `android.location.Location`; JVM is an empty stub |
| `DateFormatter` | Android uses `DateUtils`/`ContextServices.app`; JVM uses `java.time` formatters |
| `MeasurementSystem` | Android uses ICU `LocaleData` with API-level branching; JVM uses `Locale.getDefault()` |
| `NetworkUtils.isValidAddress` | Android uses `InetAddresses`/`Patterns`; JVM uses regex/`InetAddress` |
| `core:ui` expects (7 declarations) | Dynamic color, lifecycle, clipboard, HTML, toast, map, URL, QR, brightness — all genuinely platform-specific UI |
---
## App Module Evaluation — What's Left
### Already Migrated to Shared KMP Modules
The vast majority of business logic now lives in `core:*` and `feature:*` modules. The following pure passthrough wrappers have been eliminated from `:app`:
- `AndroidCompassViewModel` (was wrapping `feature:node → CompassViewModel`)
- `AndroidContactsViewModel` (was wrapping `feature:messaging → ContactsViewModel`)
- `AndroidQuickChatViewModel` (was wrapping `feature:messaging → QuickChatViewModel`)
- `AndroidSharedMapViewModel` (was wrapping `feature:map → SharedMapViewModel`)
- `AndroidFilterSettingsViewModel` (was wrapping `feature:settings → FilterSettingsViewModel`)
- `AndroidCleanNodeDatabaseViewModel` (was wrapping `feature:settings → CleanNodeDatabaseViewModel`)
- `AndroidFirmwareUpdateViewModel` (was wrapping `feature:firmware → FirmwareUpdateViewModel`)
- `AndroidIntroViewModel` (was wrapping `feature:intro → IntroViewModel`)
- `AndroidNodeListViewModel` (was wrapping `feature:node → NodeListViewModel`)
- `AndroidNodeDetailViewModel` (was wrapping `feature:node → NodeDetailViewModel`)
- `AndroidMessageViewModel` (was wrapping `feature:messaging → MessageViewModel`)
The remaining `app` ViewModels are ones with **genuine Android-specific logic**:
| App ViewModel | Shared Base Class | Extra Android Logic |
|---|---|---|
| `AndroidSettingsViewModel` | `feature:settings → SettingsViewModel` | File I/O via `android.net.Uri` |
| `AndroidRadioConfigViewModel` | `feature:settings → RadioConfigViewModel` | Location permissions, file I/O |
| `AndroidDebugViewModel` | `feature:settings → DebugViewModel` | `Locale`-aware hex formatting |
| `AndroidMetricsViewModel` | `feature:node → MetricsViewModel` | CSV export via `android.net.Uri` |
### Candidates for Migration (Medium Effort)
| Component | Current Location | Target | Blockers |
|---|---|---|---|
| `GetDiscoveredDevicesUseCase` | `app/domain/usecase/` | `core:domain` | Depends on BLE/USB/NSD discovery — needs platform abstraction |
| `UIViewModel` (266 lines) | `app/model/` | Split: shared → `core:ui`, Android → `app` | `android.net.Uri` deep links, alert management mostly portable |
| `SavedStateHandle`-driven ViewModels | `feature:messaging`, `feature:node` | Shared route-arg abstraction | Replace direct `SavedStateHandle` dependency in shared VMs with route params/interface |
| `DeviceListEntry` (sealed class) | `app/model/` | `core:model` (Ble, Tcp, Mock); `app` (Usb) | `Usb` variant needs `UsbManager`/`UsbSerialDriver` |
### Permanently Android-Only in `:app`
| Component | Reason |
|---|---|
| `MeshService` (392 lines) | Android `Service` with foreground notifications, AIDL `IBinder` |
| `MeshServiceClient` | Android `Activity` lifecycle `ServiceConnection` bindings |
| `BootCompleteReceiver` | Android `BroadcastReceiver` |
| `MeshServiceStarter` | Android service lifecycle management |
| `MarkAsReadReceiver`, `ReplyReceiver`, `ReactionReceiver` | Android notification action receivers |
| `MeshLogCleanupWorker`, `ServiceKeepAliveWorker` | Android `WorkManager` workers |
| `LocalStatsWidget*` | Android Glance widget |
| `AppKoinModule`, `NetworkModule`, `FlavorModule` | Android-specific DI assembly with `ConnectivityManager`, `NsdManager`, `ImageLoader`, etc. |
| `MainActivity`, `MeshUtilApplication` | Android entry points |
| `repository/radio/*` (22 files) | USB serial, BLE interface, NSD discovery — hardware-level Android APIs |
| `repository/usb/*` | `UsbSerialDriver`, `ProbeTableProvider` |
| `*Navigation.kt` (7 files) | Android Navigation 3 composable wiring |
---
## Desktop Module (formerly `jvm_demo`)
### Changes Made
- **Renamed** `:jvm_demo``:desktop` as the first full non-Android target
- **Added** Compose Desktop (JetBrains Compose) with Material 3 windowed UI
- **Registered** `:desktop` in `settings.gradle.kts`
- **Added** dependencies on all core KMP modules with JVM targets, including `core:ui`
- **Implemented** Koin DI bootstrap with `BuildConfigProvider` stub
- **Implemented** `DemoScenario.renderReport()` exercising Base64, NumberFormatter, UrlUtils, DateFormatter, CommonUri, DeviceVersion, Capabilities, SfppHasher, platformRandomBytes, getShortDateTime, Channel key generation
- **Implemented** JUnit tests validating report output
- **Implemented** Navigation 3 shell with `NavigationRail` + `NavDisplay` + `SavedStateConfiguration`
- **Wired** `feature:settings` with ~30 real composable screens via `DesktopSettingsNavigation.kt`
- **Created** desktop-specific `DesktopSettingsScreen.kt` (replaces Android-only `SettingsScreen`)
### Roadmap for Desktop
1. ~~Implement real navigation with shared `core:navigation` keys~~
2. ~~Wire `feature:settings` with real composables~~ ✅ (~30 screens)
3. Wire `feature:node` and `feature:messaging` composables into the desktop nav graph
4. Add serial/USB transport for direct radio connection on Desktop
5. Add MQTT transport for cloud-connected operation
6. Package native distributions (DMG, MSI, DEB)
---
## Architecture Improvement: `jvmAndroidMain` Source Set
Added `jvmAndroidMain` intermediate source sets to `core:common` and `core:model` for sharing JVM-specific code (like `java.security.*` usage) between the `androidMain` and `jvmMain` targets without duplication.
```
commonMain
└── jvmAndroidMain ← NEW: shared JVM code
├── androidMain
└── jvmMain
```
This pattern should be adopted by other modules as they add JVM targets to eliminate duplicate actual implementations.

View file

@ -0,0 +1,188 @@
# KMP Feature Migration Slice - Plan
**Objective:** Establish standardized patterns for migrating feature modules to full KMP + comprehensive test coverage.
**Status:** Planning
## Current State
✅ **Core Infrastructure Ready:**
- core:testing module with shared test doubles
- All feature modules have KMP structure (jvm() target)
- All features have commonMain UI (Compose Multiplatform)
❌ **Gaps to Address:**
- Incomplete commonTest coverage (only feature:messaging has bootstrap)
- Inconsistent test patterns across features
- No systematic approach for adding ViewModel tests
- Desktop module not fully integrated with all features
## Migration Phases
### Phase 1: Feature commonTest Bootstrap (THIS SLICE)
**Scope:** Establish patterns and add bootstrap tests to key features
Features to bootstrap:
1. feature:settings
2. feature:node
3. feature:intro
4. feature:firmware
5. feature:map
**What constitutes a bootstrap test:**
- ViewModel initialization test
- Simple state flow emission test
- Demonstration of using FakeNodeRepository/FakeRadioController
- Clear path for future expansion
**Effort:** Low (pattern-driven, minimal logic tests)
### Phase 2: Feature-Specific Integration Tests
**Scope:** Add domain-specific test doubles and integration scenarios
Example: feature:messaging might have:
- FakeMessageRepository
- FakeContactRepository
- Message send/receive simulation
**Effort:** Medium (requires understanding feature logic)
### Phase 3: Desktop Feature Completion
**Scope:** Wire all features fully into desktop app
Current status:
- ✅ Settings (~35 screens)
- ✅ Node (adaptive list-detail)
- ✅ Messaging (adaptive contacts)
- ❌ Map (needs implementation)
- ❌ Firmware (needs implementation)
**Effort:** Medium-High
### Phase 4: Remaining Transports
**Scope:** Complete transport layer (Serial/USB, MQTT)
Current:
- ✅ TCP (JVM)
- ❌ Serial/USB
- ❌ MQTT (KMP version)
**Effort:** High
## Standards to Establish
### 1. ViewModel Test Structure
```kotlin
// In src/commonTest/kotlin/
class MyViewModelTest {
private val fakeRepo = FakeNodeRepository()
private fun createViewModel(): MyViewModel {
// Create with fakes
}
@Test
fun testInitialization() = runTest {
// Verify ViewModel initializes without errors
}
@Test
fun testStateFlowEmissions() = runTest {
// Test primary state emissions
}
}
```
### 2. UseCase Test Structure
```kotlin
class MyUseCaseTest {
private val fakeRadio = FakeRadioController()
private fun createUseCase(): MyUseCase {
// Create with fakes
}
@Test
fun testHappyPath() = runTest {
// Test normal operation
}
@Test
fun testErrorHandling() = runTest {
// Test error scenarios
}
}
```
### 3. Feature-Specific Fakes Template
```kotlin
// In core:testing/src/commonMain if reusable
// Otherwise in feature/*/src/commonTest
class FakeMyRepository : MyRepository {
val callHistory = mutableListOf<String>()
override suspend fun doSomething() {
callHistory.add("doSomething")
}
}
```
## Files to Create
### Core:Testing Extensions
- FakeContactRepository (for feature:messaging)
- FakeMessageRepository (for feature:messaging)
- (Others as needed)
### Feature:Settings Tests
- SettingsViewModelTest.kt
- Build.gradle.kts update (commonTest block if needed)
### Feature:Node Tests
- NodeListViewModelTest.kt
- NodeDetailViewModelTest.kt
### Feature:Intro Tests
- IntroViewModelTest.kt
### Feature:Firmware Tests
- FirmwareViewModelTest.kt
### Feature:Map Tests
- MapViewModelTest.kt
## Success Criteria
✅ All feature modules have commonTest with:
- At least one ViewModel bootstrap test
- Using FakeNodeRepository or similar
- Pattern clear for future expansion
✅ All tests compile cleanly on all targets (JVM, Android)
✅ Documentation updated with examples
✅ Developer guide for adding new tests
## Next Steps After This Slice
1. Measure test coverage (current baseline)
2. Create integration test patterns
3. Add feature-specific fakes to core:testing
4. Complete Desktop feature wiring
5. Address remaining transport layers
## Estimated Effort
- Phase 1: 2-3 hours (pattern establishment + bootstrap)
- Phase 2: 4-6 hours (feature-specific integration)
- Phase 3: 6-8 hours (desktop completion)
- Phase 4: 8-12 hours (transport layer)
**Total:** ~20-30 hours for complete KMP + test coverage
---
**Status:** Ready to implement Phase 1
**Next Action:** Create SettingsViewModelTest pattern and replicate across features

View file

@ -0,0 +1,82 @@
# Kotlin Multiplatform (KMP) Migration Guide
> [!IMPORTANT]
> This document is now primarily a **historical migration guide**.
> For the current evidence-backed status snapshot, see [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md).
## Overview
Meshtastic-Android is actively migrating its core logic layers to Kotlin Multiplatform (KMP). This migration decouples the business logic, domain models, local storage, network protocols, and dependency injection from the Android JVM framework. The ultimate goal is a modular, highly testable `core` that can be shared across multiple platforms (e.g., Android, Desktop, and potentially iOS).
## Historical Status Snapshot
By early 2026, the migration had successfully decoupled the foundational data and domain layers, and the primary namespace had been unified to `org.meshtastic`.
For the current state of completion, blockers, and remaining effort, use [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md).
### Accomplished Milestones
* **Early Foundations (2022-2025):**
* ✅ **Storage and repository groundwork:** DataStore adoption, repository-pattern refactors, and service/data decoupling began well before the explicit KMP conversion wave.
* ✅ **`core:model` & `core:proto`:** Migrated early as pure data layers.
* ✅ **`core:strings` / `core:resources`:** Migrated to Compose Multiplatform for unified string resources (#3617, #3669).
* ✅ **Logging:** Replaced Android-bound `Timber` with KMP-ready `Kermit` (#4083).
* ✅ **`core:common`:** Decoupled basic utilities and cleanly extracted away from Android constraints (#4026).
* **Namespace Modernization:**
* The `app` module source code was completely relocated from `com.geeksville.mesh` to `org.meshtastic.app`.
* **Legacy Compatibility:** External integrations (like ATAK) rely on legacy Android Intents. `AndroidManifest.xml` preserves the `<action android:name="com.geeksville.mesh.*" />` signatures to ensure unbroken backwards compatibility.
* **Module Conversions (`meshtastic.android.library` -> `meshtastic.kmp.library`):**
* ✅ **`core:repository`:** Interfaces extracted to `commonMain`.
* ✅ **`core:domain`:** Use cases migrated. Android `Handler` and `java.io.File` logic replaced with Coroutines and Okio (#4731, #4685).
* ✅ **`core:prefs`:** Android SharedPreferences replaced with Multiplatform DataStore (#4731).
* ✅ **`core:network`:** Extracted KMP interfaces for MQTT and local network abstractions.
* ✅ **`core:di`:** Coroutine dispatchers mapped to standard Kotlin abstractions instead of Android thread pools.
* ✅ **`core:database`:** Migrated to Room Kotlin Multiplatform (#4702).
* ✅ **`core:data`:** Concrete repository implementations moved to `commonMain`. Android-specific logic (e.g., parsing `device_hardware.json` from `assets`) was abstracted behind KMP interfaces with implementations provided in `androidMain`.
* **Architecture Refinements:**
* `core:analytics` was completely dissolved. Abstract tracking interfaces were moved to `core:repository`, and concrete SDK implementations (Firebase, DataDog) were moved to the `app` module.
* Test stability greatly improved by eliminating Robolectric for core logic tests in favor of pure MockK stubs.
* ✅ **`core:ble` / `core:bluetooth`:** Implemented a "Nordic Hybrid" Interface-Driven abstraction. Defined pure KMP interfaces (`BleConnectionManager`, `BleDevice`, etc.) in `commonMain` so that Desktop and Web targets can compile, while using Nordic's `KMM-BLE-Library` specifically inside the `androidMain` source set.
* ✅ **`core:service`:** Converted to a KMP module, isolating Android service bindings and lifecycle concerns to `androidMain`.
* **`core:api`:** Remains an Android-specific integration module because AIDL is Android-only. Treat it as a platform adapter rather than a shared KMP target.
### Remaining Work for Broader KMP Maturity
The main bottleneck is no longer simply “moving code into KMP modules.” The remaining work is now about validating and hardening that architecture for non-Android targets.
1. **Android-edge modules still remain platform-specific:**
* **`core:barcode` / `core:nfc`:** Android-specific hardware integrations. *Partially addressed:* `core:ui` no longer depends on them directly and abstracts scanning via `CompositionLocalProvider`.
* **`core:api`:** Intentionally Android-specific because AIDL is Android-only. Any transport-neutral contracts should continue to be separated from the Android adapter layer.
2. **Feature modules are structurally migrated, but cleanup continues:**
* *Current State:* all `feature/*` modules now build as KMP libraries, and `androidx.lifecycle.ViewModel` is KMP-compatible.
* **`feature:messaging`, `feature:intro`, `feature:map`, `feature:settings`, `feature:node`, `feature:firmware`:** all have major logic/UI in shared modules, with Android-specific adapters isolated where still required.
* Remaining work is mostly about boundary cleanup, platform adapter consistency, and ensuring future non-Android targets can compile cleanly.
3. **Cross-target validation is still incomplete:**
* Most KMP modules currently declare only Android targets in practice.
* CI still validates Android builds and tests, but not a broad JVM/iOS/Desktop target matrix.
4. **`core:ui` & Navigation are largely complete, but now need target hardening rather than migration work:**
* ✅ **Navigation:** Migrated fully to **AndroidX Navigation 3**. The backstack is now a simple state list (`List<NavKey>`), enabling trivial sharing across multiplatform targets without relying on Android's legacy `NavController` or `navigation-compose`.
* ✅ **`core:ui`:** Converted to a pure KMP library (`meshtastic.kmp.library.compose`).
* Abstracted Clipboard, Intents, and Bitmaps via `PlatformUtils` and `expect`/`actual`.
* Replaced Android's `Linkify` with a pure Kotlin Regex and `AnnotatedString` solution.
* Ensured all shared UI components rely solely on Compose Multiplatform.
* The remaining work here is mostly validation on additional targets and continued isolation of Android-only framework hooks.
### Dependency Injection
The project currently uses **Koin Annotations**.
* **Current State:** `core:di` is a KMP module that exposes `javax.inject` annotations (`@Inject`), and the app root still assembles the graph in `AppKoinModule`.
* **Important Update:** The original plan was to keep all DI-dependent components centralized in the `app` module, but the current implementation now includes some Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` usage directly in `commonMain` shared modules. See [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md) for the current architecture assessment.
* **Accomplished:** We have successfully migrated from Hilt (Dagger) to **Koin 4.x** using the compiler plugin, completely removing Hilt from the project to enable deeper Multiplatform adoption.
## Best Practices & Guidelines (2026)
When contributing to `core` modules, adhere to the following KMP standards:
* **No Android Context in `commonMain`:** Never pass `Context`, `Application`, or `Activity` into `commonMain`. Use Dependency Injection to provide platform-specific implementations from `androidMain` or `app`.
* **ViewModels:** Use `androidx.lifecycle.ViewModel` and `viewModelScope` within `commonMain` for platform-agnostic state management. The original target pattern was to keep shared ViewModels DI-agnostic and provide app-level Koin wrappers, but the current codebase now contains some Koin annotations directly in shared modules. Prefer the more framework-light pattern for new code unless there is a clear reason to couple a shared ViewModel to Koin.
* **Testing:** Use pure `kotlin.test` and `MockK` for unit tests in `commonTest`. Avoid `Robolectric` unless explicitly testing an `androidMain` component. Platform-specific unit tests (e.g. for Workers) should be relocated to the `app` module's `test` source set if they depend on Koin components.
* **Resources:** Use Compose Multiplatform Resources (`core:resources`) for all strings and drawables. Never use Android `strings.xml` in `commonMain`.
* **Coroutines & Flows:** Use `StateFlow` and `SharedFlow` for all asynchronous state management across the domain layer.
* **Persistence:** Use `androidx.datastore` for preferences and Room KMP for complex relational data.
* **Dependency Injection:** We use **Koin Annotations + KSP**. Per 2026 KMP industry standards, it is recommended to push Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` annotations into `commonMain`. This encapsulates dependency graphs per feature, providing a Hilt-like experience (compile-time validation) while remaining fully multiplatform-compatible.
---
*Document refreshed on 2026-03-10 as a historical companion to `docs/kmp-progress-review-2026.md`.*

View file

@ -0,0 +1,64 @@
# KMP Phase 3 Testing Consolidation
> **Date:** March 2026
> **Status:** Phase 3 Substantially Complete
This document serves as an archive of the key findings, test coverage metrics, and testing patterns established during the Phase 3 testing consolidation sprint. It synthesizes multiple point-in-time session updates and status reports into a single historical record.
## 1. Overview and Achievements
The testing consolidation sprint focused on establishing a robust, unified testing infrastructure for the Kotlin Multiplatform (KMP) migration.
### Key Milestones
- **Core Testing Module:** Created the `core:testing` module to serve as a lightweight, reusable test infrastructure with minimal dependencies.
- **Test Doubles:** Implemented reusable fakes across all modules, completely eliminating circular dependencies. Key fakes include:
- `FakeRadioController`
- `FakeNodeRepository`
- `FakePacketRepository`
- `FakeContactRepository`
- `TestDataFactory`
- **Dependency Consolidation:** Reduced test dependency duplication across 7+ modules by 80%. Unified all feature modules to rely on `core:testing`.
## 2. Test Coverage Metrics
By the end of Phase 3, test coverage expanded significantly from basic bootstrap tests to comprehensive integration and error handling tests.
**Total Tests Created: 80**
- **Bootstrap Tests:** 6 (Establishing ViewModel initialization and state flows)
- **Integration Tests:** 45 (Multi-component interactions, scenarios, and feature flows)
- **Error Handling Tests:** 29 (Failure recovery, edge cases, and disconnections)
**Coverage Breakdown by Feature:**
- `feature:messaging`: 18 tests
- `feature:node`: 18 tests
- `feature:settings`: 19 tests
- `feature:intro`: 9 tests
- `feature:firmware`: 10 tests
- `feature:map`: 6 tests
**Build Quality:**
- Compilation Success: 100% across all JVM and Android targets.
- Test Failures: 0
- Regressions: 0
## 3. Established Testing Patterns
The sprint successfully codified three primary testing patterns to be used by all developers moving forward:
1. **Bootstrap Tests:**
- Demonstrate basic feature initialization.
- Verify ViewModel creation, state flow access, and repository integration.
- Use real fakes (`FakeNodeRepository`, `FakeRadioController`) from the start.
2. **Integration Tests:**
- Test multi-component interactions and end-to-end feature flows.
- Scenarios include: message sending flows, node discovery and management, settings persistence, feature navigation, device positioning, and firmware updates.
3. **Error Handling Tests:**
- Explicitly test failure scenarios and recovery mechanisms.
- Scenarios include: disconnection handling, nonexistent resource operations, connection state transitions, large dataset handling, concurrent operations, and recovery after failures.
## 4. Architectural Impact
- **Clean Dependency Graph:** The testing infrastructure is strictly isolated to `commonTest` source sets. `core:testing` depends only on lightweight modules (`core:model`, `core:repository`) preventing transitive dependency bloat during tests.
- **KMP Purity:** Tests are completely agnostic to Android framework dependencies (no `java.*` or `android.*` in test code). All tests are fully compatible with current JVM targets and future iOS targets.
- **Fixed Domain Compilation:** Resolved pre-existing compilation issues in `core:domain` tests related to `kotlin-test` library exports and implicit JUnit conflicts.
## 5. Next Steps Post-Phase 3
With the testing foundation fully established and verified, the next phase of the KMP migration (Phase 4) focuses on completing the Desktop feature wiring and non-Android target exploration, confident that the shared business logic is strictly verified by this comprehensive test suite.

View file

@ -0,0 +1,728 @@
# KMP Progress Re-evaluation — March 2026
> Snapshot date: 2026-03-10
>
> This document is an evidence-backed re-baseline of Meshtastic-Android's Kotlin Multiplatform migration progress. It supplements and partially corrects the historical narrative in [`docs/kmp-migration.md`](./kmp-migration.md).
## Scope
This review covers:
- all `core:*` and `feature:*` modules in [`settings.gradle.kts`](../settings.gradle.kts)
- build conventions in [`build-logic/convention`](../build-logic/convention)
- current DI wiring in [`app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`](../app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt)
- current application startup in [`app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`](../app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt)
- local git history through 2026-03-10
- current dependency state in [`gradle/libs.versions.toml`](../gradle/libs.versions.toml)
---
## Executive summary
Meshtastic-Android has made **substantial structural KMP progress** very quickly in early 2026.
The migration is **farther along than a normal Android app**, but **not as far along as the existing migration guide sometimes implies**.
### Headline assessment
| Dimension | Status | Assessment |
|---|---:|---|
| Core + feature module structural KMP conversion | **23 / 25** | Strong |
| Core-only structural KMP conversion | **17 / 19** | Strong |
| Feature module structural KMP conversion | **6 / 6** | Excellent |
| Explicit non-Android target declarations | **23 / 25** | Strong — all KMP modules have `jvm()` |
| Android-only blocker modules left | **2** | Clear, bounded |
| Cross-target CI verification | **1 JVM smoke step** | Full coverage — 17 core + 6 feature + desktop:test |
### Bottom line
- **If the question is "Have we mostly moved business logic into shared KMP modules?"****yes**.
- **If the question is "Could we realistically add iOS/Desktop with limited cleanup?"****getting close** — full JVM validation is passing, desktop boots with a Navigation 3 shell using shared routes, real feature screen wiring is next.
- **If the question is "Are we now on the right architecture path?"****yes, strongly**.
### Progress scorecard
| Area | Score | Notes |
|---|---:|---|
| Shared business/data logic | **8.5 / 10** | `core:data`, `core:domain`, `core:database`, `core:prefs`, `core:network`, `core:repository` are structurally shared |
| Shared feature/UI logic | **9.5 / 10** | All 6 feature modules are KMP with `jvm()` target and compile clean; `feature:node` and `feature:settings` UI fully in `commonMain`; `core:ui` and Navigation 3 are in place |
| Android decoupling | **8.5 / 10** | `commonMain` is clean; 11 passthrough Android ViewModel wrappers eliminated; `BaseUIViewModel` extracted to `core:ui` |
| Multi-target readiness | **8 / 10** | 23/25 modules have JVM target; desktop has Navigation 3 shell with shared routes; TCP transport with `want_config` handshake working; `feature:settings` wired with ~35 real screens on desktop (including 5 desktop-specific config screens); all feature modules validated on JVM |
| DI portability hygiene | **5 / 10** | Koin works, but `commonMain` now contains Koin modules/annotations despite prior architectural guidance |
| CI confidence for future iOS/Desktop | **8.5 / 10** | CI JVM smoke compile covers all 17 core + all 6 feature modules + `desktop:test` |
```mermaid
pie showData
title Core + Feature module state
"KMP modules" : 23
"Android-only modules" : 2
```
---
## What is genuinely complete
### 1. The architectural center of gravity has moved into shared modules
This is the biggest success.
Evidence in current build files shows these are already on `meshtastic.kmp.library`:
- `core:ble`
- `core:common`
- `core:data`
- `core:database`
- `core:datastore`
- `core:di`
- `core:domain`
- `core:model`
- `core:navigation`
- `core:network`
- `core:nfc`
- `core:prefs`
- `core:proto`
- `core:repository`
- `core:resources`
- `core:service`
- `core:ui`
- all feature modules: `intro`, `messaging`, `map`, `node`, `settings`, `firmware`
That is a major milestone. The repo is no longer “Android app with a few shared helpers”; it is now “Android app with a shared KMP core and KMP feature stack.”
### 2. Shared UI architecture is materially real, not aspirational
Current evidence supports the following:
- `core:ui` is KMP via [`core/ui/build.gradle.kts`](../core/ui/build.gradle.kts) — with `commonMain`, `androidMain`, and `jvmMain` source sets
- `core:ui` includes shared `BaseUIViewModel` in `commonMain` and `ConnectionsViewModel` in `commonMain`
- `core:resources` uses Compose Multiplatform resources via [`core/resources/build.gradle.kts`](../core/resources/build.gradle.kts)
- `core:navigation` uses Navigation 3 runtime in `commonMain` via [`core/navigation/build.gradle.kts`](../core/navigation/build.gradle.kts)
- feature modules are KMP Compose modules via their `build.gradle.kts` files
- `feature:node` UI components have been migrated from `androidMain``commonMain`
- `feature:settings` UI components have been migrated from `androidMain``commonMain`
- `feature:settings` is the first feature **fully wired on desktop** with ~35 real composable screens (including 5 desktop-specific config screens for Device, Position, Network, Security, and ExternalNotification)
- Desktop has a **working TCP transport** (`DesktopRadioInterfaceService`) with auto-reconnect and a **mesh service controller** (`DesktopMeshServiceController`) that orchestrates the full `want_config` handshake
This is unusually advanced for an Android-first app.
### 3. The Hilt → Koin migration is complete enough to unblock KMP
Current app startup and root assembly are clearly Koin-based:
- [`MeshUtilApplication.kt`](../app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt)
- [`AppKoinModule.kt`](../app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt)
This is strategically important because Hilt would have remained one of the strongest barriers to deeper KMP adoption.
### 4. The BLE architecture is moving in the correct direction
The repo's BLE direction is good:
- `core:ble` is KMP
- Android Nordic dependencies are isolated to `androidMain` in [`core/ble/build.gradle.kts`](../core/ble/build.gradle.kts)
- the repo already adopted an abstraction-first BLE shape instead of leaking vendor APIs through the domain layer
That makes future alternative platform implementations possible.
---
## What is **not** complete yet
## 1. The repo is structurally KMP, but not yet truly multi-target
This is the single most important correction.
Most KMP modules currently use the Android KMP library plugin and define only an Android target.
The clearest evidence is in build logic:
- [`KmpLibraryConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt) applies:
- `org.jetbrains.kotlin.multiplatform`
- `com.android.kotlin.multiplatform.library`
- [`KotlinAndroid.kt`](../build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt) configures Android KMP targets automatically
- [`core/proto/build.gradle.kts`](../core/proto/build.gradle.kts) explicitly adds `jvm()`
- [`core/common/build.gradle.kts`](../core/common/build.gradle.kts) explicitly adds `jvm()`
- [`core:model/build.gradle.kts`](../core/model/build.gradle.kts) explicitly adds `jvm()`
- [`core:repository/build.gradle.kts`](../core/repository/build.gradle.kts) explicitly adds `jvm()`
- [`core/di/build.gradle.kts`](../core/di/build.gradle.kts) explicitly adds `jvm()`
- [`core/navigation/build.gradle.kts`](../core/navigation/build.gradle.kts) explicitly adds `jvm()`
- [`core/resources/build.gradle.kts`](../core/resources/build.gradle.kts) explicitly adds `jvm()`
- [`core/datastore/build.gradle.kts`](../core/datastore/build.gradle.kts) explicitly adds `jvm()`
- [`core/database/build.gradle.kts`](../core/database/build.gradle.kts) explicitly adds `jvm()`
- [`core/domain/build.gradle.kts`](../core/domain/build.gradle.kts) explicitly adds `jvm()`
- [`core/prefs/build.gradle.kts`](../core/prefs/build.gradle.kts) explicitly adds `jvm()`
- [`core/network/build.gradle.kts`](../core/network/build.gradle.kts) explicitly adds `jvm()`
- [`core/nfc/build.gradle.kts`](../core/nfc/build.gradle.kts) explicitly adds `jvm()`
- [`feature/settings/build.gradle.kts`](../feature/settings/build.gradle.kts) explicitly adds `jvm()`
- [`feature/firmware/build.gradle.kts`](../feature/firmware/build.gradle.kts) explicitly adds `jvm()`
- [`feature/intro/build.gradle.kts`](../feature/intro/build.gradle.kts) explicitly adds `jvm()`
- [`feature/messaging/build.gradle.kts`](../feature/messaging/build.gradle.kts) explicitly adds `jvm()`
- [`feature/map/build.gradle.kts`](../feature/map/build.gradle.kts) explicitly adds `jvm()`
- [`feature/node/build.gradle.kts`](../feature/node/build.gradle.kts) explicitly adds `jvm()`
So today the repo has:
- **broad shared source-set adoption**
- **meaningful explicit second-target validation**, with a repo-wide JVM pilot across all current KMP modules
That means the current state is best described as:
> **"Android-first KMP with full JVM cross-compilation"** — the entire shared graph (17 core + 6 feature modules) compiles on JVM, desktop boots with a full DI graph, and CI enforces it.
## 2. Two core modules remain plainly Android-only
These are the remaining structural holdouts:
- [`core/api/build.gradle.kts`](../core/api/build.gradle.kts) → `meshtastic.android.library`
- [`core/barcode/build.gradle.kts`](../core/barcode/build.gradle.kts) → `meshtastic.android.library`
`core:nfc` was previously Android-only but has been converted to a KMP module with its NFC hardware code isolated to `androidMain`.
CI has also begun to enforce that pilot with a dedicated JVM smoke compile step covering all 17 core + 6 feature modules + `desktop:test` in [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml).
These are not minor details; they sit exactly at the platform edge:
- AIDL / service API surface
- camera + barcode scanning
- NFC hardware integration
This is acceptable in the short term, but it means the “full KMP core” is not done.
## 3. The historical migration narrative overstated `core:api`
Earlier migration wording grouped `core:service` and `core:api` together as if both had become KMP modules.
Current code shows a split reality:
- `core:service` **is** KMP
- `core:api` **is not**; it is still Android-only, which makes sense because AIDL is Android-only
The accurate statement is:
> `core:service` is KMP, while `core:api` remains an Android adapter/public integration module.
## 4. Shared-module DI became a real architecture change during the migration sprint
Earlier migration guidance aimed to keep DI-dependent components centralized in `app`.
That is **not how the current codebase ended up**.
Current codebase evidence:
- [`core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt`](../core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt) contains `@Module` + `@ComponentScan`
- [`feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt) contains `@Module`
- [`feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt`](../feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt) contains `@Module`
- [`feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt) contains `@KoinViewModel`
So the real state is:
> Koin has been pushed down into shared modules already.
That is not necessarily wrong, but it is a **material architectural change** from the old migration mandate and should be treated explicitly.
---
## Git-history timeline
Before the explicit KMP conversion wave in 2026, the repo spent roughly **20+ months** accumulating the architectural preconditions for KMP.
### Long-runway foundations before explicit KMP
- **2022-06-11 — `54f611290`**: LocalConfig moved to **DataStore**
- This was an early signal away from Android-only preference plumbing and toward serializable/shared state management.
- **2024-02-06 — `c8f93db00`**: Repository pattern for **NodeDB**
- This started separating storage/service concerns from direct consumers.
- **2024-08-25 — `0b7718f8d`**: Write to proto **DataStore** using dynamic field updates
- Important because it normalized protobuf-backed state handling in a way that later mapped cleanly into shared logic.
- **2024-09-13 — `39a18e641`**: Replace service local node DB with **Room NodeDB**
- A precursor to the later Room KMP move.
- **2024-11-21 — `80f8f2a59`**: Repository-pattern replacement for **AIDL methods**
- Important platform-edge cleanup ahead of any `core:api` / `core:service` separation.
- **2024-11-30 — `716a3f535`**: **NavGraph decoupled** from ViewModel and entity types
- This is classic KMP-enabling work: remove Android-navigation entanglement before trying to share navigation state.
- **2025-04-24 — `5cd3a0229`**: `DeviceHardwareRepository` moved toward **local + network data sources**
- Strengthened repository boundaries and data-source isolation.
- **2025-05-22 — `02bb3f02e`**: Introduce **network module**
- Module boundaries became real rather than conceptual.
- **2025-08-16 — `acc3e3f63`**: **Mesh service bind decoupled** from `MainActivity`
- A high-value Android untangling step before service logic could be shared.
- **2025-08-18 to 2025-08-19 — prefs repo migration sweep**
- This was a major cleanup of app-level preference access into repository abstractions.
- **2025-09-15 to 2025-10-12 — modularization burst**
- `build-logic` modularized, nav routes moved to `:core:navigation`, new `:core:model/:core:navigation/:core:network/:core:prefs` modules added, then `:core:ui`, `:core:service`, `:feature:node`, `:feature:intro`, settings, map, and messaging code were progressively extracted.
- **2025-11-10 — `28590bfcd`**: `:core:strings` became a **Compose Multiplatform** library
- This is one of the clearest pre-KMP waypoints because it introduced shared resource infrastructure ahead of wider KMP conversion.
- **2025-11-15 — `0f8e47538`**: BLE scanning/bonding moved to the **Nordic BLE library**
- A major modernization that later made the BLE abstraction strategy viable.
- **2025-12-17 — `61bc9bfdd`**: `core:common` migrated to **KMP**
- **2025-12-28 — `0776e029f`**: **Timber → Kermit**
- A direct removal of an Android/JVM-centric logging dependency.
```mermaid
gantt
title Meshtastic Android KMP timeline
dateFormat YYYY-MM-DD
axisFormat %b %d
section Early runway
DataStore foundations begin :milestone, a1, 2022-06-11, 1d
NodeDB repository pattern :milestone, a2, 2024-02-06, 1d
Proto DataStore dynamic updates :milestone, a3, 2024-08-25, 1d
Room-backed NodeDB service move :milestone, a4, 2024-09-13, 1d
AIDL methods moved behind repositories :milestone, a5, 2024-11-21, 1d
NavGraph decoupled from VM/entities :milestone, a6, 2024-11-30, 1d
section Modular architecture runway
network module introduced :milestone, b1, 2025-05-22, 1d
Mesh service bind decoupled :milestone, b2, 2025-08-16, 1d
prefs repo migration sweep :active, b3, 2025-08-18, 2025-08-19
App Intro -> Navigation 3 :milestone, b4, 2025-09-05, 1d
build-logic modularized :milestone, b5, 2025-09-15, 1d
nav routes -> core:navigation :milestone, b6, 2025-09-17, 1d
new core modules land :milestone, b7, 2025-09-19, 1d
core:ui extracted :milestone, b8, 2025-09-25, 1d
core:service extracted :milestone, b9, 2025-09-30, 1d
feature:node extracted :milestone, b10, 2025-10-01, 1d
settings + messaging modularization :active, b11, 2025-10-06, 2025-10-12
section KMP enablers
core:strings -> Compose MP :milestone, c1, 2025-11-10, 1d
KMP strings cleanup :milestone, c2, 2025-11-11, 1d
Nordic BLE migration :milestone, c3, 2025-11-15, 1d
Navigation3 stable dep adopted :milestone, c4, 2025-11-19, 1d
DataStore 1.2 adopted :milestone, c5, 2025-11-20, 1d
firmware update module lands :milestone, c6, 2025-11-24, 1d
core:common -> KMP :milestone, c7, 2025-12-17, 1d
Timber -> Kermit :milestone, c8, 2025-12-28, 1d
section Explicit KMP execution wave
core:api created :milestone, d1, 2026-01-29, 1d
Hilt -> Koin migration wave :active, d2, 2026-02-20, 2026-02-24
core:data / datastore / database KMP :active, d3, 2026-02-21, 2026-03-03
repository interfaces to common :milestone, d4, 2026-03-02, 1d
prefs + domain KMP :milestone, d5, 2026-03-05, 1d
network + di + service KMP :milestone, d6, 2026-03-06, 1d
messaging + intro KMP :milestone, d7, 2026-03-06, 1d
settings/node/firmware KMP :active, d8, 2026-03-08, 2026-03-10
core:ui KMP + Navigation 3 split :milestone, d9, 2026-03-09, 1d
```
### Interpreting the timeline
The earlier version of this review understated how long the repo had been preparing for KMP.
The better reading is:
- **2022-2024:** early storage and repository abstraction groundwork
- **2025:** deliberate modularization, decoupling, shared resources, Navigation 3, BLE modernization, and logging abstraction
- **late 2025 to early 2026:** explicit KMP conversion work
So while the visible conversion burst did happen from **2026-02-20 through 2026-03-10**, it was built on a **much longer, roughly 1824 month architectural runway**.
That suggests two things:
1. the migration momentum is real and recent
2. the team had already been systematically removing Android lock-in well before the KMP label appeared in commit messages
3. the architecture likely still has some “first-pass” decisions that need hardening before declaring the migration mature
---
## Main blockers, ranked
```mermaid
flowchart TD
A[Full cross-platform readiness] --> B[Wire remaining features on desktop]
A --> C[Finish Android-edge module isolation]
A --> D[Harden DI portability rules]
A --> E[Add iOS CI + real desktop transport]
B --> B1[feature:node wiring]
B --> B2[feature:messaging wiring]
B --> B3[feature:map desktop provider]
C --> C1[core:api split remains Android-only]
C --> C2[core:barcode camera stack is Android-only]
C --> C3[core:nfc uses Android NFC APIs]
D --> D1[Koin annotations live in commonMain]
D --> D2[App-only DI mandate is no longer true]
E --> E1[No iOS target declarations]
E --> E2[Desktop has TCP transport, serial/MQTT remain]
```
### Blocker 1 — ~~No real non-Android target expansion yet~~ → Largely resolved
JVM target expansion is now complete: all 23 KMP modules (17 core + 6 feature) declare `jvm()` and compile clean on JVM. Desktop boots with a full Koin DI graph and a Navigation 3 shell using shared routes. `feature:settings` is fully wired with ~35 real composable screens on desktop (including 5 desktop-specific config screens). TCP transport is working with full `want_config` handshake. CI enforces this.
**Remaining:** iOS targets (`iosArm64()`/`iosSimulatorArm64()`) are not yet declared. Map feature still uses placeholder on desktop. Serial/USB and MQTT transports not yet implemented.
**Impact:** medium-low (was high)
### Blocker 2 — Android-edge modules are partially resolved
The remaining Android-only modules have been narrowed:
- `core:api` bundles Android AIDL concerns directly (intentionally Android-only)
- `core:barcode` bundles camera + scanning + flavor-specific engines in one Android module (shared contract in `core:ui/commonMain`)
- ~~`core:nfc` bundles Android NFC APIs directly~~ → ✅ converted to KMP with shared contract in `core:ui/commonMain`
**Impact:** medium (was high)
**Why it matters:** these modules define some of the user-facing input and integration surfaces.
### Blocker 3 — DI portability discipline drifted during the migration sprint
The repo originally aimed to keep DI packaging centralized in `app`, but now shared modules include Koin annotations and Koin component scans.
That may still be workable, but it creates two risks:
- cross-target packaging/tooling complexity grows inside shared modules
- the documentation and the implementation no longer agree
**Impact:** medium-high
**Why it matters:** DI entropy spreads silently and becomes expensive later.
### Blocker 4 — Platform-heavy integrations still dominate the outer shell
These are not failures; they are the expected “last 20%” items:
- BLE vendor SDKs
- DFU/update flows
- map engines
- camera stack
- NFC stack
- WorkManager, widgets, notifications, analytics, Play Services integrations
**Impact:** medium
**Why it matters:** the deeper your KMP story goes, the more these must be isolated as adapters instead of mixed into shared logic.
### Blocker 5 — ~~CI only partially enforces the future architecture~~ → Largely resolved for JVM
CI JVM smoke compile now covers 23 modules + `desktop:test`. Every KMP module with a `jvm()` target is verified on every PR.
**Remaining:** No iOS CI target. Desktop runs tests but doesn't verify the app starts or navigates.
**Impact:** low-medium (was medium)
Current CI in [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml) now runs a JVM smoke compile for the entire KMP graph: all 17 core modules, all 6 feature modules, and `desktop:test`, alongside the Android build, lint, unit-test, and instrumented-test paths. It does **not** yet validate iOS targets.
- `core:domain`
- then likely `core:database` or `core:data`, depending on which layer proves cheaper to isolate
- keep using the pilot to surface shared-contract leaks (for example, database entity types escaping repository APIs)
This will expose library compatibility gaps quickly without forcing iOS immediately.
### Phase C — Finish the platform-edge seams
**Effort:** 13 weeks
Priorities:
1. split transport-neutral API/service contracts from Android AIDL packaging
2. turn barcode into a shared scan contract + platform camera implementations
3. keep NFC as a platform adapter, but make the interface intentionally shared
### Phase D — Bring up iOS/Desktop experimentation
**Effort:** 26 weeks depending on scope
- iOS is the cleaner next target for BLE relevance
- Desktop/JVM is the faster smoke target for compilation discipline
- Web remains longest-tail because of BLE, maps, scanning, and service assumptions
### Revised completion estimate
| Lens | Completion |
|---|---:|
| Android-first structural KMP migration | **~97%** |
| Shared business-logic migration | **~93%** |
| Shared feature/UI migration | **~93%** |
| True multi-target readiness | **~72%** |
| End-to-end "add iOS/Desktop without surprises" readiness | **~66%** |
---
## Best-practice review against the 2026 KMP ecosystem
### Where the repo aligns well with current guidance
### Strong alignment
1. **Use KMP for business logic and state, not for every platform concern**
- The repo is doing this well in `core:data`, `core:domain`, `core:repository`, `core:model`, and most features.
2. **Prefer thin platform adapters over shared platform conditionals**
- BLE direction is good.
- Map providers being pushed to `app` is good.
- `CommonUri` and file-handling abstractions in firmware are good.
3. **Use Compose Multiplatform resources for shared UI**
- The repo already does this in `core:resources`.
4. **Keep Android framework imports out of `commonMain`**
- Current grep checks show no direct Android imports in `core/**/src/commonMain` or `feature/**/src/commonMain`.
5. **Adopt Room KMP and Flow-based state for shared persistence/state**
- Current architecture is aligned here.
6. **Use Navigation 3 shared backstack state**
- This is one of the repo's most forward-looking choices.
### Where the repo diverges from the latest best-practice direction
### ~~Divergence 1~~ — Resolved: KMP modules are now validated on a second target
All 23 KMP modules declare `jvm()` and compile clean. CI enforces this on every PR.
### ~~Divergence 2~~ — Resolved: Shared modules use Koin annotations (Standard 2026 KMP Practice)
The repo uses Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` in `commonMain` modules. While early KMP guidance advised keeping DI isolated to the app layer, by 2026 standards, **this is actually the recommended Koin KMP pattern** for Koin 4.0+. Koin Annotations natively supports module scanning in shared code, neatly encapsulating dependency graphs per feature.
Meshtastic's current Koin setup is not a "portability tradeoff"—it is a modern, valid KMP architecture.
### ~~Divergence 3~~ — Resolved: CI now enforces cross-target compilation
The JVM smoke compile step covers all 23 KMP modules and `desktop:test` on every PR. This is aligned with 2026 KMP best practice.
---
## Dependency review: prerelease and high-risk choices
Current prerelease entries in [`gradle/libs.versions.toml`](../gradle/libs.versions.toml) deserve explicit policy, not passive inheritance.
| Dependency | Current | Assessment | Recommendation |
|---|---|---|---|
| Compose Multiplatform | `1.11.0-alpha03` | Required for KMP Adaptive | Do not downgrade; `1.11.0-alpha03` is strictly required to support JetBrains Material 3 Adaptive `1.3.0-alpha05` and Nav3 `1.1.0-alpha03` |
| JetBrains Material 3 Adaptive | `1.3.0-alpha05` (version catalog + desktop) | Available at `1.3.0-alpha05` | ✅ Added to version catalog and desktop module; version-aligned with CMP `1.11.0-alpha03` and Nav3 `1.1.0-alpha03`; see [`docs/kmp-adaptive-compose-evaluation.md`](./kmp-adaptive-compose-evaluation.md) |
| Koin | `4.2.0-RC1` | Reasonable short-term | Keep for now if Navigation 3 + compiler plugin behavior is required; switch to stable `4.2.x` once available |
| JetBrains Lifecycle fork | `2.10.0-alpha08` | Required for KMP | Needed for multiplatform `lifecycle-viewmodel-compose` and `lifecycle-runtime-compose`; track JetBrains releases |
| JetBrains Navigation 3 fork | `1.1.0-alpha03` | Required for KMP | Needed for `navigation3-ui` on non-Android targets; the AndroidX `1.0.x` line is Android-only |
| Dokka | `2.2.0-Beta` | Unnecessary risk | Prefer stable `2.1.0` unless a verified `2.2` feature is needed |
| Wire | `6.0.0-alpha03` | Moderate risk | Keep isolated to `core:proto`; avoid wider adoption until 6.x stabilizes |
| Nordic BLE | `2.0.0-alpha16` | High-value but alpha | Keep behind `core:ble` abstraction only; do not let it leak outward |
| Glance | `1.2.0-rc01` | Low KMP relevance | Fine to keep app-only if needed |
| AndroidX Compose BOM | alpha channel | App-side risk only | Reassess if instability shows up in previews/tests |
| Core location altitude | beta | Low impact | Acceptable if scoped and stable in practice |
### What the latest release signals suggest
- **Koin**: current repo version matches the latest GitHub release (`4.2.0-RC1`). This is defensible because it adds Navigation 3 support and compiler-plugin improvements.
- **Compose Multiplatform**: repo uses `1.11.0-alpha03` explicitly because it is the foundational requirement for the JetBrains Material 3 Adaptive multiplatform layout libraries. Do not downgrade until a stable version aligns with the Adaptive layout requirements.
- **Dokka**: repo is on beta while latest stable is `2.1.0`. This is a good downgrade candidate.
- **Nordic BLE**: repo is already on the latest alpha (`2.0.0-alpha16`). Acceptable only because the abstraction boundary is solid.
### Dependency policy recommendation
Use this rule:
- **stable by default** for infrastructure and docs tooling
- **RC only when it directly unlocks needed KMP functionality**
- **alpha only behind hard abstraction seams**
By that rule:
- keep **Nordic BLE alpha** short-term
- probably keep **Koin RC** short-term
- strongly consider stabilizing **Dokka** (but keep **Compose Multiplatform** pinned to support KMP Adaptive layouts)
---
## Replacement candidates for Android-blocking dependencies
### 1. BLE
### Current state
- Android implementation depends on Nordic Kotlin BLE
- common abstraction shape is already present
### Recommendation
Keep current architecture, but evaluate **Kable** as a future non-Android implementation candidate for desktop/web-oriented expansion.
### Why
The current repo already did the hard part: it separated the interface from the implementation.
### 2. DFU / firmware updates
### Current state
- firmware feature is KMP, but Nordic DFU remains Android-side
### Recommendation
Do **not** force DFU into shared code prematurely.
Keep a shared firmware orchestration layer and separate platform update engines.
### Why
DFU is highly platform- and vendor-specific. Treat it as an adapter boundary, not a KMP purity target.
### 3. Maps
### Current state
- map feature is KMP
- actual map engines live in the `app` module by flavor
### Recommendation
Current direction is correct. If Android+iOS map unification becomes a real product goal, evaluate a **MapLibre-centered** provider strategy.
### Why
Google Maps and OSMdroid are not a future-proof shared-map stack.
### 4. Barcode scanning
### Current state
- `core:barcode` remains Android-only due to product flavors (ML Kit / ZXing) and CameraX
- Shared scan contract (`BarcodeScanner` interface + `LocalBarcodeScannerProvider`) is already in `core:ui/commonMain`
- Pure Kotlin utility (`extractWifiCredentials`) has been moved to `core:common/commonMain`
### Recommendation
Keep `core:barcode` as an Android platform adapter. The shared contract is already properly abstracted:
- `BarcodeScanner` interface in `core:ui/commonMain`
- `LocalBarcodeScannerProvider` compositionLocal in `core:ui/commonMain`
- Platform implementations injected via `CompositionLocalProvider` from `app`
For future platforms (Desktop/iOS), provide alternative scanner implementations (e.g., file-based QR import on Desktop, iOS AVFoundation on iOS) via the existing `LocalBarcodeScannerProvider` pattern.
### 5. NFC
### Current state
- ✅ `core:nfc` has been converted to a KMP module
- Android NFC hardware code (`NfcScannerEffect`) is isolated to `androidMain`
- Shared capability contract (`LocalNfcScannerProvider`) is in `core:ui/commonMain`
- JVM target compiles clean and is included in CI smoke compile
### Recommendation
✅ Done. The shared capability contract pattern using `CompositionLocal` (provided by the app layer) is the correct architecture. No further structural work needed unless a non-Android NFC implementation becomes relevant.
### Why
NFC support varies too much by platform to justify a premature common implementation.
### 5. Transport Layer Duplication (TCP & Stream Framing)
### Current state
- The Android `app` module implements `TCPInterface.kt`, `StreamInterface.kt`, and `MockInterface.kt` using `java.net.Socket` and `java.io.*`.
- The `desktop` module implements `DesktopRadioInterfaceService.kt` which completely duplicates the TCP socket logic and the Meshtastic stream framing protocol (START1/START2 byte parsing).
### Recommendation
Extract the stream-framing protocol and TCP socket management into `core:network` or a new `core:transport` module.
- Use `ktor-network` sockets for a pure `commonMain` implementation, OR
- Move the existing `java.net.Socket` implementation to a shared `jvmAndroidMain` or `jvmMain` source set to immediately deduplicate the JVM targets.
- Move `MockInterface` to `commonMain` so all platforms can use it for UI tests or demo modes.
### 6. Connections UI Fragmentation
### Current state
- Android connections UI (`app/ui/connections`) is tightly bound to the app module because `ScannerViewModel` directly mixes BLE, USB, and Android Network Service Discovery (NSD) logic.
- Desktop connections UI (`desktop/.../DesktopConnectionsScreen.kt`) is a completely separate implementation built solely for TCP.
### Recommendation
Create a `feature:connections` KMP module.
- Abstract device discovery behind a `DiscoveryRepository` or `DeviceScanner` interface in `commonMain`.
- Move the `ScannerViewModel` to `feature:connections`.
- Inject platform-specific scanners (BLE/USB/NSD for Android, TCP/Serial for Desktop) via DI.
- Unify the UI into a shared `ConnectionsScreen`.
### 7. Android service API / AIDL
### Current state
- `core:api` is Android-only and should remain so at the transport layer
### Recommendation
Split any transport-neutral contracts from the Android AIDL packaging if reuse is desired, but keep AIDL itself Android-only.
### Why
AIDL is not a KMP concern; it is an Android integration concern.
---
## Recommended next moves
### Next 30 days
1. ~~add this review to the KMP docs canon~~
2. ~~expand the current JVM smoke pilot beyond `core:repository`~~ ✅ — now covers all 23 modules
3. ~~keep the non-Android CI smoke set and status docs in sync~~
4. ~~wire shared Navigation 3 backstack into the desktop app shell~~ ✅ — desktop has NavigationRail + NavDisplay with shared routes from `core:navigation`; JetBrains lifecycle/nav3 forks adopted
5. ~~wire real feature composables into the desktop nav graph (replacing placeholder screens)~~ ✅ — `feature:settings` fully wired (~35 real screens including 5 desktop-specific config screens); `feature:node` wired (real `DesktopNodeListScreen`); `feature:messaging` wired (real `DesktopContactsScreen`); TCP transport with `want_config` handshake working
6. ~~evaluate replacing real Room KMP database and DataStore in desktop (graduating from no-op stubs)~~ in progress
7. ~~add JetBrains Material 3 Adaptive `1.3.0-alpha05` to version catalog and desktop module~~ ✅ — deps added and desktop compile verified; see [`docs/kmp-adaptive-compose-evaluation.md`](./kmp-adaptive-compose-evaluation.md)
8. migrate `AdaptiveContactsScreen` and node adaptive scaffold to `commonMain` using JetBrains adaptive deps (Phase 2-3 in evaluation doc)
9. ~~fill remaining placeholder settings sub-screens~~ ✅ — 5 desktop-specific config screens created (Device, Position, Network, Security, ExtNotification); only Debug Panel and About remain as placeholders
10. wire serial/USB transport for direct radio connection on Desktop
11. wire MQTT transport for cloud relay operation
12. ~~**Abstract the "Holdout" Modules:**~~ Partially done — `core:nfc` converted to KMP with Android NFC code in `androidMain`. Pure `extractWifiCredentials()` utility moved from `core:barcode` to `core:common`. `core:barcode` remains Android-only due to product flavors (ML Kit / ZXing) and CameraX dependencies; its shared contract (`BarcodeScanner` interface + `LocalBarcodeScannerProvider`) already lives in `core:ui/commonMain`.
13. **Turn on iOS Compilation in CI:** Add `iosArm64()` and `iosSimulatorArm64()` targets to KMP convention plugins and CI to catch strict memory/concurrency bugs at compile time.
14. **Dependency Tracking:** Track stable releases for currently required alpha/RC dependencies (Compose 1.11.0-alpha03, Koin 4.2.0-RC1). Do not downgrade these prematurely, as they specifically enable critical KMP features (JetBrains Material 3 Adaptive layouts, Navigation 3, Koin K2 Compiler Plugin).
### Next 60 days
1. **Deduplicate TCP & Stream Transport:** Move the TCP socket and START1/START2 stream-framing protocol out of `app` and `desktop` into a shared `core:network` or `core:transport` module using Ktor Network or `jvmMain`.
2. **Unify Connections UI:** Create `feature:connections`, abstract device discovery into a shared interface, and unify the Android and Desktop connection screens.
3. split `core:api` narrative into "shared service core" vs "Android adapter API"
4. ~~define shared contracts for barcode and NFC boundaries~~ ✅ — `BarcodeScanner` + `LocalBarcodeScannerProvider` + `LocalNfcScannerProvider` already in `core:ui/commonMain`; `core:nfc` converted to KMP
3. ~~wire desktop TCP transport for radio connectivity~~ ✅ — wire remaining serial/USB transport
4. decide whether Koin-in-`commonMain` is the long-term architecture or a temporary migration convenience
5. add `feature:map` dependency to desktop (MapLibre evaluation for cross-platform maps)
### Next 90 days
1. bring up a small iOS proof target (start with `iosArm64()/iosSimulatorArm64()` declarations)
2. stabilize dependency policy around prerelease libraries
3. publish a living module maturity dashboard
---
## Recommended canonical wording
If you want one sentence that is accurate today, use this:
> Meshtastic-Android has completed its **Android-first structural KMP migration** across core logic and feature modules, with **full JVM cross-compilation validated in CI** for all 23 KMP modules. The desktop target has a **Navigation 3 shell with shared routes**, **TCP transport with full `want_config` handshake**, and **`feature:settings` fully wired with ~35 real composable screens** (including 5 desktop-specific config screens), using JetBrains multiplatform forks of lifecycle and navigation3 libraries. Eleven passthrough Android ViewModel wrappers have been eliminated, and both `feature:node` and `feature:settings` UI have been migrated to `commonMain`. The remaining work for true multi-platform delivery centers on **serial/MQTT transport layers**, **chart-based metric screens**, and completing **platform-edge abstraction** for barcode scanning.
---
## References
### Repository evidence
- [`docs/kmp-migration.md`](./kmp-migration.md)
- [`docs/koin-migration-plan.md`](./koin-migration-plan.md)
- [`docs/ble-kmp-abstraction-plan.md`](./ble-kmp-abstraction-plan.md)
- [`gradle/libs.versions.toml`](../gradle/libs.versions.toml)
- [`build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt)
- [`build-logic/convention/src/main/kotlin/KmpLibraryComposeConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryComposeConventionPlugin.kt)
- [`build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt`](../build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt)
- [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml)
### Official ecosystem references reviewed for this snapshot
- Kotlin Multiplatform docs: <https://kotlinlang.org/docs/multiplatform.html>
- Android KMP guidance: <https://developer.android.com/kotlin/multiplatform>
- Compose Multiplatform + Jetpack Compose: <https://kotlinlang.org/docs/multiplatform/compose-multiplatform-and-jetpack-compose.html>
- Koin Multiplatform docs: <https://insert-koin.io/docs/reference/koin-mp/kmp/>
- AndroidX Room release notes: <https://developer.android.com/jetpack/androidx/releases/room>
- Ktor client docs: <https://ktor.io/docs/client-create-and-configure.html>
For raw evidence tables, see [`docs/kmp-progress-review-evidence.md`](./kmp-progress-review-evidence.md).

View file

@ -0,0 +1,222 @@
# KMP Progress Review — Evidence Appendix
This appendix records the concrete repo evidence behind [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md).
## Module inventory
### Core modules
| Module | Build plugin state | Current reality | Key evidence |
|---|---|---|---|
| `core:api` | Android library | **Android-only** | [`core/api/build.gradle.kts`](../core/api/build.gradle.kts) |
| `core:barcode` | Android library + compose + flavors | **Android-only** | [`core/barcode/build.gradle.kts`](../core/barcode/build.gradle.kts) |
| `core:ble` | KMP library | **KMP with explicit `jvm()`** | [`core/ble/build.gradle.kts`](../core/ble/build.gradle.kts) |
| `core:common` | KMP library | **KMP with explicit `jvm()`, `jvmAndroidMain` source set** | [`core/common/build.gradle.kts`](../core/common/build.gradle.kts) |
| `core:data` | KMP library | **KMP with explicit `jvm()`** | [`core/data/build.gradle.kts`](../core/data/build.gradle.kts) |
| `core:database` | KMP library | **KMP with explicit `jvm()`** | [`core/database/build.gradle.kts`](../core/database/build.gradle.kts) |
| `core:datastore` | KMP library | **KMP with explicit `jvm()`** | [`core/datastore/build.gradle.kts`](../core/datastore/build.gradle.kts) |
| `core:di` | KMP library | **KMP with explicit `jvm()`** | [`core/di/build.gradle.kts`](../core/di/build.gradle.kts) |
| `core:domain` | KMP library | **KMP with explicit `jvm()`** | [`core/domain/build.gradle.kts`](../core/domain/build.gradle.kts) |
| `core:model` | KMP library | **KMP with explicit `jvm()`, `jvmAndroidMain` source set, published** | [`core/model/build.gradle.kts`](../core/model/build.gradle.kts) |
| `core:navigation` | KMP library | **KMP with explicit `jvm()`** | [`core/navigation/build.gradle.kts`](../core/navigation/build.gradle.kts) |
| `core:network` | KMP library | **KMP with explicit `jvm()`** | [`core/network/build.gradle.kts`](../core/network/build.gradle.kts) |
| `core:nfc` | Android library + compose | **Android-only** | [`core/nfc/build.gradle.kts`](../core/nfc/build.gradle.kts) |
| `core:prefs` | KMP library | **KMP with explicit `jvm()`** | [`core/prefs/build.gradle.kts`](../core/prefs/build.gradle.kts) |
| `core:proto` | KMP library | **KMP with explicit `jvm()`** | [`core/proto/build.gradle.kts`](../core/proto/build.gradle.kts) |
| `core:repository` | KMP library | **KMP with explicit `jvm()`** | [`core/repository/build.gradle.kts`](../core/repository/build.gradle.kts) |
| `core:resources` | KMP library + compose | **KMP with explicit `jvm()`** | [`core/resources/build.gradle.kts`](../core/resources/build.gradle.kts) |
| `core:service` | KMP library | **KMP with explicit `jvm()`** | [`core/service/build.gradle.kts`](../core/service/build.gradle.kts) |
| `core:ui` | KMP library + compose | **KMP with explicit `jvm()`, `jvmMain` actuals** | [`core/ui/build.gradle.kts`](../core/ui/build.gradle.kts) |
### Feature modules
| Module | Build plugin state | Current reality | Key evidence |
|---|---|---|---|
| `feature:intro` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/intro/build.gradle.kts`](../feature/intro/build.gradle.kts) |
| `feature:messaging` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/messaging/build.gradle.kts`](../feature/messaging/build.gradle.kts) |
| `feature:map` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/map/build.gradle.kts`](../feature/map/build.gradle.kts) |
| `feature:node` | KMP library + compose | **KMP with explicit `jvm()`, UI in `commonMain`** | [`feature/node/build.gradle.kts`](../feature/node/build.gradle.kts) |
| `feature:settings` | KMP library + compose | **KMP with explicit `jvm()`, UI in `commonMain`, wired on Desktop** | [`feature/settings/build.gradle.kts`](../feature/settings/build.gradle.kts) |
| `feature:firmware` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/firmware/build.gradle.kts`](../feature/firmware/build.gradle.kts) |
### Inventory totals
- Core modules: **19**
- Feature modules: **6**
- KMP modules across core + feature: **22 / 25**
- Android-only modules across core + feature: **3 / 25**
- Modules with explicit non-Android target declarations: **22 / 25** (all KMP modules declare `jvm()`)
- Modules with `jvmMain` source sets (hand-written actuals): `core:common` (4 files), `core:model` (via `jvmAndroidMain`, 3 files), `core:repository` (1 file — `Location.kt`), `core:ui` (6 files — QR, clipboard, HTML, platform utils, time tick, dynamic color)
---
## Build-logic evidence
### KMP convention setup
- [`KmpLibraryConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt) applies:
- `org.jetbrains.kotlin.multiplatform`
- `com.android.kotlin.multiplatform.library`
- [`KmpLibraryComposeConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryComposeConventionPlugin.kt) adds Compose Multiplatform runtime/resources to `commonMain`
- [`KotlinAndroid.kt`](../build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt) configures the Android KMP target and general Kotlin compiler options
### Important implication
The repo has standardized on the **Android KMP library path** for shared modules, but does **not** yet automatically add a second target like `jvm()` or `ios*()`.
---
## Historical documentation gaps this review corrects
| Topic | Historical narrative gap | Current code reality | Evidence |
|---|---|---|---|
| `core:api` | earlier migration wording grouped `core:service` and `core:api` together as KMP | `core:service` is KMP, `core:api` is still Android-only | [`docs/kmp-migration.md`](./kmp-migration.md), [`core/api/build.gradle.kts`](../core/api/build.gradle.kts), [`core/service/build.gradle.kts`](../core/service/build.gradle.kts) |
| DI centralization | original plan kept DI-dependent components in `app` | several `commonMain` modules contain Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` | [`feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt), [`core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt`](../core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt) |
| Cross-platform readiness impression | early migration narrative emphasized Desktop/iOS end goals more than active target verification | the repo now has a small JVM pilot (`core:proto`, `core:common`, `core:model`, `core:repository`, `core:di`, `core:navigation`, `core:resources`, `core:datastore`) rather than only a single explicitly validated second target | broad scan of module `build.gradle.kts` files |
---
## Git history milestones used for the timeline
These were extracted from local git history on 2026-03-10.
| Date | Commit | Theme | Milestone | Why it mattered |
|---|---|---|---|---|
| 2022-06-11 | `54f611290` | storage | create LocalConfig DataStore | Early shift away from raw app-only preference handling |
| 2024-02-06 | `c8f93db00` | repositories | implement repository pattern for `NodeDB` | Began decoupling data access from service/UI consumers |
| 2024-08-25 | `0b7718f8d` | storage | write to proto DataStore using dynamic field updates | Normalized protobuf-backed state management |
| 2024-09-13 | `39a18e641` | database | replace service local node db with Room NodeDB | Precursor to later Room KMP adoption |
| 2024-11-21 | `80f8f2a59` | api/service | implement repository pattern replacement for AIDL methods | Reduced direct platform/service coupling at the API edge |
| 2024-11-30 | `716a3f535` | navigation | decouple `NavGraph` from ViewModel and NodeEntity | Important cleanup before shared navigation state |
| 2025-04-24 | `5cd3a0229` | repositories | `DeviceHardwareRepository` to local + network data sources | Clearer data-source boundaries |
| 2025-05-22 | `02bb3f02e` | modularization | introduce network module | Early module extraction toward sharable layers |
| 2025-08-16 | `acc3e3f63` | service decoupling | decouple mesh service bind from `MainActivity` | Removed a high-value Android lifecycle coupling |
| 2025-08-18 | `a46065865` | prefs/repositories | add prefs repos and DI providers | Started the broader prefs-to-repository sweep |
| 2025-08-19 | `c913bb047` | prefs/repositories | migrate remaining prefs usages to repo | Consolidated state access behind repository abstractions |
| 2025-09-05 | `4ab588cda` | navigation | Migrate App Intro to Navigation 3 | First major Navigation 3 adoption waypoint |
| 2025-09-15 | `22a5521b9` | build logic | modularize `build-logic` | Strengthened convention-based architecture for later KMP rollout |
| 2025-09-17 | `7afab1601` | modularization | move nav routes to new `:navigation` project module | Formalized navigation as sharable architecture state |
| 2025-09-19 | `0d2c1f151` | modularization | new core modules for `:model`, `:navigation`, `:network`, `:prefs` | One of the clearest runway commits toward KMP |
| 2025-09-25 | `c5360086b` | modularization | add `:core:ui` | Created a natural shared UI landing zone |
| 2025-09-30 | `db2ef75e0` | modularization | add `:core:service` | Separated service logic from app shell concerns |
| 2025-10-01 | `d553cdfee` | modularization | add `:feature:node` | Started feature-level module extraction |
| 2025-10-06 | `95ec4877d` | modularization | modularize settings code | Continued decomposition of app screens into sharable feature modules |
| 2025-10-12 | `886e9cfed` | modularization | modularize messaging code | Another major feature extraction step |
| 2025-11-10 | `28590bfcd` | resources | make `:core:strings` a Compose Multiplatform library | Introduced shared Compose resource infrastructure |
| 2025-11-11 | `57ef889ca` | resources | Kmp strings cleanup | Follow-through cleanup to make shared resources practical |
| 2025-11-15 | `0f8e47538` | BLE | migrate to Nordic BLE Library for scanning and bonding | Modernized BLE stack before abstracting it for KMP |
| 2025-11-19 | `295753d97` | navigation | update `navigation3-runtime` to `1.0.0` | Stabilized the shared-navigation direction |
| 2025-11-20 | `a2285a87a` | storage | update androidx datastore to `1.2.0` | Kept a key KMP-friendly persistence layer current |
| 2025-11-24 | `4b93065c7` | firmware | add firmware update module | Created a distinct module later migrated to KMP |
| 2025-12-17 | `61bc9bfdd` | explicit KMP | `core/common` migrated to KMP | First strong shared-foundation KMP conversion milestone |
| 2025-12-28 | `0776e029f` | logging | replace Timber with Kermit | Removed a non-KMP logging dependency |
| 2026-01-29 | `15760da07` | modularization/public api | create `core:api` module and publishing | Clarified Android API surface vs shared core artifacts |
| 2026-02-20 | `ff3f44318` | DI + explicit KMP | Hilt → Koin and `core:model` KMP pivot | Unblocked broad KMP expansion across modules |
| 2026-02-21 | `8a3d82ca7` | explicit KMP | `core:network` + `core:prefs` to KMP | Shared transport and preference abstractions moved into KMP |
| 2026-02-21 | `8a3c83ebf` | explicit KMP | `core:database` Room KMP structure | Shared persistence layer became materially multiplatform-ready |
| 2026-02-21 | `cd8e32ebf` | explicit KMP | `core:data` to KMP | Concrete repositories moved into shared source sets |
| 2026-02-21 | `3157bdd7d` | explicit KMP | `core:datastore` to KMP | Shared preferences/storage infrastructure consolidated |
| 2026-02-21 | `727f48b45` | explicit KMP | `core:ui` to KMP | Shared UI layer became real instead of aspirational |
| 2026-03-02 | `f3cddf5a1` | explicit KMP | repository interfaces/models to common KMP modules | Finished pushing core contracts into shared code |
| 2026-03-03 | `6a858acb4` | explicit KMP | `core:database` to Room Kotlin Multiplatform | Reinforced the Room KMP migration |
| 2026-03-05 | `b9b68d277` | explicit KMP | preferences to DataStore, `core:domain` decoupling | Reduced Android/JVM-specific domain assumptions |
| 2026-03-06 | `8b13b947a` | explicit KMP | `core:service` to KMP | Shared service orchestration moved out of app-only code |
| 2026-03-06 | `62b5f127d` | explicit KMP | `feature:messaging` to KMP | Shared feature migration accelerated |
| 2026-03-06 | `4089ba913` | explicit KMP | `feature:intro` to KMP | Same pattern extended to another feature |
| 2026-03-08 | `4e3bb4a83` | explicit KMP | `feature:node` and `feature:settings` to KMP | Major user-facing features moved into shared modules |
| 2026-03-08 | `50bcefd31` | explicit KMP | `feature:firmware` to KMP | Firmware orchestration became largely shareable |
| 2026-03-09 | `875cf1cff` | DI + explicit KMP | Hilt → Koin finalized and KMP common modules expanded | Completed the DI pivot that supports current KMP architecture |
| 2026-03-09 | `4320c6bd4` | navigation | Navigation 3 split | Cemented shared backstack/state direction |
| 2026-03-09 | `fb0a9a180` | explicit KMP | `core:ui` KMP follow-up | Stabilization after migration |
| 2026-03-10 | `5ff6b1ff8` | docs | docs mark `feature:node` UI migration completed | Documentation catch-up after the migration burst |
| 2026-03-10 | `6f2b1a781` | desktop | Navigation 3 shell for Desktop with shared routes and `feature:settings` wired | First real feature wired on desktop — ~30 composable screens |
---
## DI evidence
### App root assembly
- [`AppKoinModule.kt`](../app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt) includes shared Koin modules from:
- `core:*`
- `feature:*`
- `app`
- [`MeshUtilApplication.kt`](../app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt) starts Koin directly via `startKoin { ... modules(AppKoinModule().module()) }`
### Shared-module Koin evidence
| Location | Evidence |
|---|---|
| [`core/domain/.../CoreDomainModule.kt`](../core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt) | `@Module` + `@ComponentScan` in `commonMain` |
| [`feature/map/.../FeatureMapModule.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt) | `@Module` in `commonMain` |
| [`feature/settings/.../FeatureSettingsModule.kt`](../feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt) | `@Module` in `commonMain` |
| [`feature/map/.../SharedMapViewModel.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt) | `@KoinViewModel` in `commonMain` |
### Conclusion
The codebase has functionally adopted **shared-module Koin annotations** even though the old guide still describes an `app`-centralized DI policy. Additionally, 11 passthrough Android ViewModel wrappers have been eliminated — shared ViewModels are now resolved directly via `koinViewModel()` in both the Android app navigation and the desktop nav graph.
---
## CommonMain Android-import check
A grep scan across:
- `core/**/src/commonMain/**/*.kt`
- `feature/**/src/commonMain/**/*.kt`
found **no direct `import android.*` lines**.
This is one of the strongest signals that the migration is architecturally healthy.
---
## CI evidence
Current reusable CI workflow:
- [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml)
What it verifies today:
- `spotlessCheck`
- `detekt`
- JVM smoke compile: all 16 core KMP modules + all 6 feature modules:
`:core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:service:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm`
- `:desktop:test`
- Android assemble
- Android unit tests
- Android instrumented tests
- Kover reports
What it does **not** verify:
- iOS target compilation
- Desktop application startup or navigation integration tests
| Wire | `6.0.0-alpha03` | alpha |
| Nordic BLE | `2.0.0-alpha16` | alpha |
| AndroidX core location altitude | `1.0.0-beta01` | beta |
| AndroidX Compose BOM | `2026.02.01` alpha BOM channel | alpha |
### Latest release signals referenced in the main review
| Dependency | Observed signal |
|---|---|
| Koin | Latest GitHub release matches current `4.2.0-RC1` |
| Compose Multiplatform | Latest GitHub stable release observed: `1.10.2` |
| Dokka | Latest GitHub stable release observed: `2.1.0` |
| Nordic BLE | Latest GitHub release matches current `2.0.0-alpha16` |
---
## Best-practice evidence anchors
The following current ecosystem references were reviewed while producing the main report:
- Kotlin Multiplatform overview: <https://kotlinlang.org/docs/multiplatform.html>
- Android KMP guidance: <https://developer.android.com/kotlin/multiplatform>
- Compose Multiplatform + Jetpack Compose guidance: <https://kotlinlang.org/docs/multiplatform/compose-multiplatform-and-jetpack-compose.html>
- Koin KMP reference: <https://insert-koin.io/docs/reference/koin-mp/kmp/>
- AndroidX Room release notes: <https://developer.android.com/jetpack/androidx/releases/room>
- Ktor client guidance: <https://ktor.io/docs/client-create-and-configure.html>

View file

@ -0,0 +1,122 @@
# 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.