CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

kmp-architecture

Kotlin Multiplatform source-set rules for Meshtastic-Android - where commonMain ends, how expect/actual is bridged, and the networking, database and platform-integration boundaries. Use this whenever you add a file to a KMP module, move code between source sets, or hit a compile error that appears on only one target.

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Skill: KMP Architecture & Source-Set Bridging

Description

Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstractions, networking, database, and platform integration rules.

1. Source-Set Boundaries

  • commonMain: All business logic, DB entities, API network logic, ViewModels, and UI rendering. NO java.* or android.* imports.
  • androidMain: Android framework integration (Context, system services, NFC hardware, BLE Android bindings).
  • jvmMain: Desktop-only JVM code.
  • jvmAndroidMain: JVM code shared between Android and Desktop. Uses the meshtastic.kmp.jvm.android convention plugin to bridge jvm and android source sets without manual dependsOn hacks.
  • androidApp / desktopApp: Host shells. Responsible for Koin DI root wiring (MainKoinModule/AppKoinModule, DesktopKoinModule), host-level UI themes, and running the MeshtasticNavDisplay.

2. Bridging Strategies

  • Interface + DI (Preferred): Expose an interface in core:repository or core:ui (e.g. LocationRepository, MapViewProvider), implement it in androidMain or the host androidApp, and bind it via Koin or CompositionLocal.
  • expect/actual (Restricted): Use only when a platform API cannot be abstracted cleanly (e.g. low-level File I/O mappings, uppercase() Locale helpers). Avoid deep class hierarchies using expect/actual.
    • Naming: An expect declaration and its shared helpers must live in differently named files within the same package, or the JVM target fails with duplicate class errors. Live example: LogExporter.kt (expect) alongside LogFormatter.kt (shared helpers).
  • Shared Helpers: Do not duplicate pure Kotlin logic between androidMain and jvmMain. Extract to a commonMain helper.

3. Core Libraries & Constraints

  • Concurrency: kotlinx.coroutines. Use org.meshtastic.core.common.util.ioDispatcher over Dispatchers.IO directly. Inject CoroutineDispatchers from core:di into classes that need dispatchers — never reference Dispatchers.IO/Main/Default directly in business logic.
  • Error Handling: Use safeCatching {} from core:common instead of runCatching {} in coroutine/suspend contexts. runCatching swallows CancellationException, breaking structured concurrency. Keep runCatching only in cleanup/teardown code (abort, close, eviction loops).
  • Standard Library Replacements:
    • ConcurrentHashMap -> atomicfu or Mutex-guarded mutableMapOf().
    • java.util.concurrent.locks.* -> kotlinx.coroutines.sync.Mutex.
    • java.io.* -> Okio (BufferedSource/BufferedSink).
  • Networking: Pure Ktor in shared code. The Android host's HttpClient runs on Ktor's OkHttp engine (the platform HttpURLConnection engine crashes on canceled gzip reads); desktop uses the Java engine. The one direct OkHttp use is the google flavor's map-tile client (MapTileHttpClient); add no others. Ktor Logging plugin for debugging.
  • HTTP Configuration: Use HttpClientDefaults from core:network for shared base URL (API_BASE_URL), timeouts, and retry constants. Both the Android (androidApp/.../di/NetworkModule.kt) and Desktop (desktopApp/.../di/DesktopRuntimeModule.kt) HttpClient instances must use these. Feature API services use relative paths; DefaultRequest sets the base URL.
  • BLE: Route through core:ble using Kable.
  • Room KMP: Use factory = { MeshtasticDatabaseConstructor.initialize() } in Room.databaseBuilder.

4. Hierarchy & Source-Set Conventions

  • Hierarchy template first: Prefer Kotlin's default hierarchy template and convention plugins over manual dependsOn(...) graphs. Manual source-set wiring should be reserved for cases the template cannot model.
  • expect/actual restraint: Prefer interfaces + DI for platform capabilities; use expect/actual for small unavoidable platform primitives. Avoid broad expect/actual class hierarchies when an interface-based boundary is sufficient.
  • Shared helpers over duplicated lambdas: When androidMain and jvmMain contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract to commonMain. Examples: formatLogsTo(), handleNodeAction(), findNodeByNameSuffix(), MeshtasticAppShell, BaseRadioTransportFactory.

5. Dependency Catalog Aliases

  • JetBrains fork aliases: Version catalog aliases for JetBrains-forked AndroidX artifacts use the jetbrains-* prefix (e.g., jetbrains-lifecycle-runtime-compose, jetbrains-navigation3-ui). Plain androidx-* aliases are true Google AndroidX artifacts. Never mix them up in commonMain. commonMain takes an androidx-* alias only where AndroidX publishes the artifact as KMP itself: androidx-navigation3-runtime, DataStore, Room, SQLite bundled and Paging.
  • Compose Multiplatform: Version catalog aliases for Compose Multiplatform artifacts use the compose-multiplatform-* prefix (e.g., compose-multiplatform-material3, compose-multiplatform-foundation). Never use plain androidx.compose dependencies in commonMain.
  • Dependencies: Always check gradle/libs.versions.toml before assuming a library is available.

6. I/O & Serialization

  • Okio standard: This project standardizes on Okio (BufferedSource/BufferedSink). JetBrains recommends kotlinx-io (built on Okio), but this project has not migrated. Do not introduce kotlinx-io without an explicit decision.
  • Room KMP: Always use factory = { MeshtasticDatabaseConstructor.initialize() } in Room.databaseBuilder and inMemoryDatabaseBuilder. DAOs and Entities reside in commonMain.
  • Room Patterns:
    • Use @Upsert for insert-or-update operations instead of manual INSERT OR IGNORE + UPDATE logic.
    • Use LIMIT 1 on @Query methods that expect a single row.
    • Prevent N+1 queries: batch operations with @Upsert fun putAll(items: List<T>) or chunked WHERE IN queries (chunk size ≤ 999 to respect SQLite bind parameter limit).

7. Build-Logic Conventions

  • In build-logic/convention, prefer lazy Gradle configuration (configureEach, withPlugin, provider APIs). Avoid afterEvaluate in convention plugins unless there is no viable lazy alternative.

8. Onboarding a New Target (Desktop/iOS)

  1. Ensure all new logic compiles against the KMP core targets (jvm(), iosSimulatorArm64()).
  2. Do not use platform-specific constructs in commonMain or you break the iOS/Desktop builds.
  3. Test using kmpSmokeCompile to verify cross-platform compilation.
  4. For desktop wiring, copy the pattern in desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt and use NoopStubs.kt to temporarily mock missing platform implementations.

Reference Anchors

  • Shared Okio I/O: core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt
  • Desktop DI Stubs: desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt
  • Version Catalog: gradle/libs.versions.toml
  • Convention Plugins: build-logic/convention/
Repository

meshtastic/Meshtastic-Android

Path

.skills/kmp-architecture/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.