ericrisco/rsc-harness/compose-multiplatform
- community
Checking that the SKILL.md file is still in the repository.
Use when building one shared Compose UI in Kotlin across Android, iOS, and desktop — commonMain @Composables, expect/actual, source-set placement, native interop, multiplatform ViewModel/navigation/Koin. NOT a single-platform native build (that is kotlin-android / swift-ios), and NOT Dart/Flutter cross-platform UI (that is flutter).
Install
npx vskill@latest install ericrisco/rsc-harness/compose-multiplatformSKILL.md
Compose Multiplatform
You write one Compose UI tree in commonMain and let each platform be a thin host. The whole discipline is one sentence: common by default, platform by exception. Every line you put in commonMain ships to Android, iOS, and desktop unchanged; every line you put in a platform source set is a deliberate exception you should be able to justify.
Versions floor (2026)
Pin these or the K2 compiler bites you. Verify against current docs before locking a project — these are the floors, not opinions.
- Compose Multiplatform 1.11.0, bundling Jetpack Compose 1.11.1.
- Kotlin 2.1.0+ required (2.2.20 recommended for evolving iOS/Web targets). Since CMP 1.8.0 the K2 compiler is mandatory, so every dependency must compile against Kotlin 2.1.0+.
- iOS is Stable (production-ready since CMP 1.8.0, May 2025): feature parity for popular cases, type-safe navigation with deep linking, accessibility (VoiceOver, Full Keyboard Access).
- Web is Beta (CMP 1.9.0, Sept 2025), runs on WasmGC browsers. Do not promise Web parity — ship Android/iOS/desktop, pilot Web.
- Platform minimums: Android API 21, iOS 14+, macOS 13 arm64, Windows 10+, Ubuntu 20.04+, desktop JDK 11+ (17+ for
jpackagepackaging).
Where does this code go?
This is the question you answer dozens of times a day. Default to the leftmost column that compiles.
| Source set | Put here | Concrete example | Never here | |
|---|---|---|---|---|
commonMain | Shared @Composables, ViewModels, business logic, common interfaces, expect declarations | @Composable fun GreetingScreen(), expect fun platformName(): String | android.*, platform.UIKit, java.awt, androidx.activity | |
androidMain | Activity, actual using Android Context/Build | class MainActivity : ComponentActivity | iOS/desktop-only APIs | |
iosMain | ComposeUIViewController factory, actual via cinterop/platform.* | fun MainViewController() = ComposeUIViewController { App() } | android.* | |
desktopMain | application {} window, Swing interop | application { Window(::exitApplication) { App() } } | mobile-only APIs | |
wasmJsMain (Beta) | Web entry point | ComposeViewport(document.body!!) { App() } | anything you can't ship as Beta |
Why this matters: a platform import in commonMain breaks the build for every other target, and the error surfaces in the iOS link step, far from the offending line. Keep commonMain import-clean.
Project structure (the 2026 default)
The current default KMP layout (announced May 2026, aligned with AGP 9.0) is a dedicated `shared` KMP library module + per-platform app modules, not the old single composeApp:
Split rule: if some screens are native and only some are shared Compose, split into `sharedLogic` (all platforms) + `sharedUI` (CMP platforms only). A server-inclusive project adds a root `core` module. Don't pre-split — start with one shared module and split when a platform genuinely needs native UI.
Source-set hierarchy — commonMain fans out, with intermediate sets where targets share code:
Scaffold a new project with kmp.new or the Kotlin Multiplatform wizard (IntelliJ IDEA 2025.2.2+ / Android Studio Otter 2025.2.1+ with the KMP plugin). Add a shared module to an existing Android app via Android Studio's Shared Module Template.
Minimal version-catalog plugin wiring (full Gradle in references/project-setup.md):
expect / actual — the core mechanism
expect/actual is how you reach a platform API while keeping the call site common. Declare expect in commonMain; provide an actual in every target you compile.
Rules, each with the reason it exists:
- Every `expect` needs an `actual` in every compiled target. An orphan
expectis not a warning — it is a hard build failure (often only surfacing on the iOS target), so add theactualper target or remove the target. - Keep the common surface tiny. Each
expectsymbol multiplies into Nactuals you maintain; expose the smallest function, not a fat class. - Prefer a common `interface` + DI over deep `expect` trees for anything you want to test or fake.
expect classcan't be mocked in common tests.
Native interop
You bridge in both directions. Shared Compose embeds native views; native hosts embed shared Compose.
- iOS — native view inside shared Compose:
UIKitView/UIKitViewControllerwith a factory lambda. - iOS — shared Compose inside SwiftUI: wrap
ComposeUIViewControllerin aUIViewControllerRepresentable. - Android:
AndroidViewfor native views; host the tree viasetContent { App() }in anActivity. - Desktop:
application { Window { App() } }; Swing interop viaSwingPanel.
Embed a native view through an injected interface, not a raw expect — so the common screen stays platform-agnostic and testable:
Full bridge patterns (ComposeUIViewController SwiftUI wrapper, native-view-factory-via-Koin, MapKit/camera, ViewModel lifecycle) live in references/ios-interop.md — read it before writing iOS interop.
State, ViewModel, navigation, DI
- `androidx.lifecycle.ViewModel` works in `commonMain`. Obtain instances with
koin-compose-viewmodel'skoinViewModel { }so they survive recomposition. iOS has no built-in `ViewModelStoreOwner` — tie the VM lifecycle to SwiftUI manually (KMP-ObservableViewModel lets SwiftUI observe Kotlin VMs). - Koin is the common DI runtime. Define a shared
initKoin()and call it from the AndroidApplicationand from iOS app init:
- Navigation:
androidx.navigationprovides type-safe nav + deep links incommonMain. - Resources:
compose.components.resourcesgeneratesResaccessors —Res.string.app_name,Res.drawable.logo, fonts — shared across all platforms.
Running & packaging
- Android: run the
androidApprun config (hosts viasetContent). - iOS: open
iosAppin Xcode, or use the KMP iOS run config in the IDE. - Desktop:
./gradlew :desktopApp:run; package with./gradlew :desktopApp:packageDistributionForCurrentOS(needs JDK 17+ forjpackage). - Web (Beta):
./gradlew :webApp:wasmJsBrowserDevelopmentRun.
Anti-patterns
| Anti-pattern | Why it bites | Do instead | |
|---|---|---|---|
android.* / platform.UIKit / java.awt import in commonMain | Breaks the build for every other target, error surfaces far away | expect/actual or inject via a common interface | |
expect with no actual for a target | Hard build failure on that target | Add an actual per compiled target or drop the target | |
Recreating a ViewModel each recomposition (remember { VM() } wrong) | State loss on every recompose | koinViewModel { } / hoist state | |
| Treating Compose Web as production | Web is Beta (1.9), not Stable | Ship Android/iOS/desktop; pilot Web only | |
| Kotlin < 2.1.0 with CMP 1.8+ | K2 incompatibility — deps fail to link | Bump to Kotlin 2.2.x | |
Deep expect class for testable logic | Can't fake in commonTest | Common interface + Koin-injected platform impl | |
Pre-splitting into sharedLogic/sharedUI on day one | Premature complexity, extra Gradle wiring | Start with one shared module; split when a platform needs native UI |
Verify
After scaffolding or editing, run scripts/verify.sh <project-dir> (read-only, no Gradle/Xcode needed). It statically checks the structural invariants:
- a
commonMainsource set exists; - every
expectincommonMainhas a matchingactualin some platform source set (catches orphans); - the Compose Multiplatform plugin (
org.jetbrains.compose) and a Kotlin version are present, and Kotlin is >= 2.1.0 (K2 floor); - no forbidden platform imports leak into
commonMain.
It exits 0 on a clean or empty target and non-zero only on hard failures.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/compose-multiplatform/SKILL.md
Popularity
- Repository stars
- 141
- Repository forks
- 11
- 7-day trend score
- 30
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/ericrisco/rsc-harness/compose-multiplatform)