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.
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstractions, networking, database, and platform integration rules.
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.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.
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).androidMain and jvmMain. Extract to a commonMain helper.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.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).ConcurrentHashMap -> atomicfu or Mutex-guarded mutableMapOf().java.util.concurrent.locks.* -> kotlinx.coroutines.sync.Mutex.java.io.* -> Okio (BufferedSource/BufferedSink).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.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.core:ble using Kable.factory = { MeshtasticDatabaseConstructor.initialize() } in Room.databaseBuilder.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.androidMain and jvmMain contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract to commonMain. Examples: formatLogsTo(), handleNodeAction(), findNodeByNameSuffix(), MeshtasticAppShell, BaseRadioTransportFactory.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-* prefix (e.g., compose-multiplatform-material3, compose-multiplatform-foundation). Never use plain androidx.compose dependencies in commonMain.gradle/libs.versions.toml before assuming a library is available.BufferedSource/BufferedSink). JetBrains recommends kotlinx-io (built on Okio), but this project has not migrated. Do not introduce kotlinx-io without an explicit decision.factory = { MeshtasticDatabaseConstructor.initialize() } in Room.databaseBuilder and inMemoryDatabaseBuilder. DAOs and Entities reside in commonMain.@Upsert for insert-or-update operations instead of manual INSERT OR IGNORE + UPDATE logic.LIMIT 1 on @Query methods that expect a single row.@Upsert fun putAll(items: List<T>) or chunked WHERE IN queries (chunk size ≤ 999 to respect SQLite bind parameter limit).build-logic/convention, prefer lazy Gradle configuration (configureEach, withPlugin, provider APIs). Avoid afterEvaluate in convention plugins unless there is no viable lazy alternative.jvm(), iosSimulatorArm64()).commonMain or you break the iOS/Desktop builds.kmpSmokeCompile to verify cross-platform compilation.desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt and use NoopStubs.kt to temporarily mock missing platform implementations.core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.ktdesktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.ktgradle/libs.versions.tomlbuild-logic/convention/meshtastic/Meshtastic-Android
1bedc5d
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.