Re-evaluation of project modularity and architecture against modern KMP and Android best practices. Identifies gaps and actionable improvements across modularity, reusability, clean abstractions, DI, and testing.
The codebase is **~98% structurally KMP** — 18/20 core modules and 8/8 feature modules declare `jvm()` targets and cross-compile in CI. Shared `commonMain` code accounts for ~52K LOC vs ~18K platform-specific LOC (a 74/26 split). This is strong.
1.**`app` is a God module** — originally 90 files / ~11K LOC of transport, service, UI, and ViewModel code that should live in core/feature modules. *(✅ Resolved — app module reduced to 6 files: `MainActivity`, `MeshUtilApplication`, Nav shell, and DI config)*
2.~~**Radio transport layer is app-locked**~~ — ✅ Resolved: `RadioTransport` interface in `core:repository/commonMain`; shared `StreamFrameCodec` + `TcpTransport` in `core:network`.
The `app` module should be a thin shell (~20 files): `MainActivity`, DI assembly, nav host. Originally it held **90 files / ~11K LOC**, now completely reduced to a **6-file shell**:
- Desktop uses the shared `ConnectionsScreen` + `CommonGetDiscoveredDevicesUseCase` directly
- Dynamic transport detection via `RadioInterfaceService.supportedDeviceTypes`
- Module registered in both `AppKoinModule` and `DesktopKoinModule`
### A4. `core:api` AIDL coupling
`core:api` is Android-only (AIDL IPC). `ServiceClient` in `core:service/androidMain` wraps it. Desktop doesn't use it — it has `DirectRadioControllerImpl` in `core:service/commonMain`.
**Recommendation:** The `DirectRadioControllerImpl` pattern is correct. Ensure `RadioController` (already in `core:model/commonMain`) is the canonical interface; deprecate the AIDL-based path for in-process usage.
---
## B. KMP Platform Purity
### B1. `java.util.Locale` leaks in `commonMain` *(resolved 2026-03-11)*
| File | Usage |
|---|---|
| `core:data/.../TracerouteHandlerImpl.kt` | Replaced with `NumberFormatter.format(seconds, 1)` |
| `core:data/.../NeighborInfoHandlerImpl.kt` | Replaced with `NumberFormatter.format(seconds, 1)` |
| `core:prefs/.../MeshPrefsImpl.kt` | Replaced with locale-free `uppercase()` |
**Outcome:** The three `Locale` usages identified in March were removed from `commonMain`. Follow-up cleanup in the same sprint also moved `ReentrantLock`-based `SyncContinuation` to `jvmAndroidMain`, replaced prefs `ConcurrentHashMap` caches with atomic persistent maps, and pushed enum reflection behind `expect`/`actual` so no known `java.*` runtime calls remain in `commonMain`.
### B2. `ConcurrentHashMap` leaks in `commonMain` *(resolved 2026-03-11)*
Formerly found in 3 prefs files:
-`core:prefs/.../MeshPrefsImpl.kt`
-`core:prefs/.../UiPrefsImpl.kt`
-`core:prefs/.../MapConsentPrefsImpl.kt`
**Outcome:** These caches now use `AtomicRef<PersistentMap<...>>` helpers in `commonMain`, eliminating the last `ConcurrentHashMap` usage from shared prefs code.
Vico chart screens (DeviceMetrics, EnvironmentMetrics, SignalMetrics, PowerMetrics, PaxMetrics) have been migrated to `feature:node/commonMain` using Vico's KMP artifacts (`vico-compose`, `vico-compose-m3`). Desktop wires them via shared composables. No Android-only chart code remains.
Also fixed `Dispatchers.IO` usage in `StoreForwardPacketHandlerImpl` (would break iOS), removed dead `UIViewModel.currentAlert` property, and added `firebase-debug.log` to `.gitignore`.
`DesktopKoinModule.kt` originally had ~120 lines of hand-written `viewModel { ... }` blocks. These have been successfully replaced by including Koin modules from `commonMain` generated via the Koin K2 Compiler Plugin for automatic wiring.
**Outcome:** Added `DesktopKoinTest.kt` using Koin's `verify()` API. This test validates the entire Desktop DI graph (including platform stubs and DataStores) during the build. Discovered and fixed missing stubs for `CompassHeadingProvider`, `PhoneLocationProvider`, and `MagneticFieldProvider`.
Android uses `@Module`-annotated classes (`CoreDataModule`, `CoreBleAndroidModule`). Desktop imports them as `CoreDataModule().coreDataModule()`. This works but the double-invocation pattern is non-obvious.
**Recommendation:** Document the pattern in AGENTS.md. Consider if Koin Annotations 2.x supports a simpler import syntax.
---
## D. Test Architecture
### D1. Zero `commonTest` in feature modules *(resolved 2026-03-12)*
**Outcome:** All 8 feature modules now have `commonTest` coverage (193 shared tests). Combined with 70 platform unit tests and 18 instrumented tests, feature modules have 281 tests total.
`core:testing` module established with shared fakes (`FakeNodeRepository`, `FakeServiceRepository`, `FakeRadioController`, `FakePacketRepository`) and `TestDataFactory` builders. Used by all feature `commonTest` suites.
Room KMP's `setAutoCloseTimeout` API is Android-only. On JVM/Desktop, once a Room database is built, its SQLite connections (5 per WAL-mode DB: 4 readers + 1 writer) remain open indefinitely until explicitly closed via `RoomDatabase.close()`.
### Problem
When a user switches between multiple mesh devices, the previous device's database remained open in the in-memory cache. Each idle database consumed ~32 MB (connection pool + prepared statement caches), leading to unbounded memory growth proportional to the number of devices ever connected in a session.
### Solution
`DatabaseManager.switchActiveDatabase()` now explicitly closes the previously active database via `closeCachedDatabase()` before activating the new one. The closed database is removed from the in-memory cache but its file is preserved, allowing transparent re-opening on next access.
Additional fixes applied:
1.**Init-order bug**: `dbCache` was declared after `currentDb`, causing NPE during `stateIn`'s `initialValue` evaluation. Reordered to ensure `dbCache` is initialized first.
2.**Corruption handlers**: `ReplaceFileCorruptionHandler` added to `createDatabaseDataStore()` on both JVM and Android, preventing DataStore corruption from crashing the app.
3.**`desktopDataDir()` deduplication**: Made public in `core:database/jvmMain` and removed the duplicate from `DesktopPlatformModule`, establishing a single source of truth for the desktop data directory.
4.**DataStore scope consolidation**: Replaced two separate `CoroutineScope` instances with a single shared `dataStoreScope` in `DesktopPlatformModule`.
5.**Coil cache path**: Desktop `Main.kt` updated to use `desktopDataDir()` instead of hardcoded `user.home`.