From android-skills
Designs Kotlin Multiplatform boundaries — expect/actual, common interfaces with platform bindings, source-set hierarchies, and Compose Multiplatform interop for accessing platform APIs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/android-skills:kmp-boundariesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Core rules for any KMP boundary:
Core rules for any KMP boundary:
commonMain semantic — describe what the product needs, not Android/iOS mechanics: currentRegion(), never currentRegionFromAndroidLocale(context).Clipboard, ShareSheet, Haptics, Biometrics as separate interfaces, not one Platform god object.if/when inside an actual belongs in common, tested with a fake.interface + per-platform binding over expect class whenever you need fakes / DI / lifecycle / runtime selection.skikoMain, appleMain) only when two platforms genuinely share an actual.Two boundaries get the most detail below: the Activity-owned platform-UI boundary, and the AGP-9 KMP-library constraints.
Related: android-skills:kmp-ktor (network boundary), compose/references/multiplatform.md (Compose-MP mechanics), android-skills:kotlin-coroutines (scope ownership). For the iOS↔Swift bridge — Kotlin→Swift naming, type widths (Int is 32-bit), SKIE suspend→async / Flow→AsyncSequence, sealed-class exhaustiveness, SwiftUI embedding — load references/ios-interop.md when authoring the iOS-side actual.
The single most common Android boundary mistake: passing applicationContext / LocalContext.current into a binding that actually needs an Activity, then papering over the lifecycle gap with Intent.FLAG_ACTIVITY_NEW_TASK. That flag is a smell — it hides that this is a foreground-UI operation. Hold an Activity instead.
// commonMain — semantic interface; DOCUMENT what `suspend` means
interface ShareSheet {
/** Launches the system share sheet. Returns when the sheet is PRESENTED — not when the user
* completes or cancels. (Otherwise callers write incorrect retry/confirmation logic.) */
suspend fun shareText(text: String)
}
// androidMain — thin: build the intent and launch it. Activity-owned.
class AndroidShareSheet(private val activity: Activity) : ShareSheet {
override suspend fun shareText(text: String) {
activity.startActivity(Intent.createChooser(
Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, text), null,
))
}
}
You don't app-wide-inject an Activity (it's framework-created and lifecycle-bound) — construct the binding in an activity scope in the Android app module (Hilt @InstallIn(ActivityComponent::class), where Activity is a default binding; Koin scoped). commonMain only ever sees the interface; the Activity never leaves the app module. If a longer-lived (app-scoped) object needs it, hold it behind a lifecycle-aware provider (set in onResume, cleared in onPause) so a destroyed Activity can't leak.
AGP 9 replaces com.android.library with com.android.kotlin.multiplatform.library for the Android side of a KMP module, and rejects com.android.application + kotlin.multiplatform outright. The new plugin enforces a single-variant architecture:
BuildConfig is unavailable — compile-time constants come from BuildKonfig or an injected AppConfiguration interface. Don't design commonMain APIs that assume BuildConfig.X exists.com.android.library module, wrapped behind a common interface the KMP module consumes.androidResources { enable = true } inside kotlin { android { … } }, or Res.string.* / Res.drawable.* crash at runtime on Android (the build still succeeds — easy to miss).consumerProguardFiles("rules.pro") from the old android {} block is silently dropped; use consumerProguardFiles.add(file("rules.pro")) in the new DSL.com.android.application — the Android entry point (MainActivity, Application class, launcher manifest, applicationId / targetSdk / versionCode / versionName) moves to a separate androidApp module that depends on the shared library. MainActivity, app-level Hilt setup, and nav-host wiring all move out of the shared androidMain.com.android.legacy-kapt for processors with no KSP equivalent.| Concern | Pre-AGP-9 (monolithic) | AGP 9 KMP library |
|---|---|---|
MainActivity, Application class, launcher manifest | androidMain of shared module | Separate androidApp module |
applicationId, versionCode, targetSdk | Shared module's android {} | androidApp only |
| Compile-time constants (env, flags) | BuildConfig field | BuildKonfig in common, or runtime DI |
| NDK / JNI native code | androidMain (any module) | Separate com.android.library, behind a common interface |
For migrating an existing project, see JetBrains' kotlin-tooling-agp9-migration skill for the full mechanics.
npx claudepluginhub rcosteira79/android-skills --plugin android-skillsGuides Kotlin Multiplatform expect/actual boundary design for platform services, native SDKs, and Compose Multiplatform UI. Keeps common APIs semantic and actuals thin.
Statically reviews KMP source-set architecture, expect/actual pairing, platform-API leakage, dependency compatibility, Swift/ObjC interop annotations, and Kotlin/Native memory model correctness.
Migrates Kotlin Multiplatform projects from the old single-module (composeApp) structure to the new shared + separate app-module layout. Handles AGP 9.0 compliance, stale IDE configurations, and project restructuring.