Meshtastic-Android/docs/archive/kmp-app-migration-assessment.md
James Rich 84bb6d24e4
docs: summarize KMP migration progress and architectural decisions (#4770)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-13 02:23:25 +00:00

127 lines
7.9 KiB
Markdown

# 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.