kmp-compose-multiplatform/SKILL.md
Compose Multiplatform for shared UI across Android, iOS, Desktop (JVM), and Web (wasmJs). Covers targets configuration, shared composables, desktop windowing, wasmJs browser targets, navigation, platform-specific UI hooks, CI/CD for multi-target builds, and performance optimisation. Companion to kmp-development (business logic) and android-development (Android-only UI).
npx skillsauth add peterbamuhigire/skills-web-dev kmp-compose-multiplatformInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
kmp-compose-multiplatform or would be better handled by a more specific companion skill.SKILL.md first, then load only the referenced deep-dive files that are necessary for the task.| Approach | When to Choose | |---|---| | Compose Multiplatform (this skill) | Design system consistency > platform feel; internal tools; desktop+mobile parity needed | | Native UI (kmp-development) | App Store/Play Store quality expected; platform-specific UX; iOS gestures critical |
Compose Multiplatform shares UI code across Android, iOS, Desktop, and Web. It is not a replacement for SwiftUI when platform-native feel matters.
// shared/build.gradle.kts (or composeApp/build.gradle.kts)
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
androidTarget()
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
jvm("desktop") // Desktop target
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
moduleName = "composeApp"
browser {
commonWebpackConfig {
outputFileName = "composeApp.js"
}
}
binaries.executable()
}
sourceSets {
val desktopMain by getting
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.navigation.compose)
implementation(libs.koin.compose)
}
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
}
}
}
compose.desktop {
application {
mainClass = "com.example.app.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.example.app"
packageVersion = "1.0.0"
}
}
}
composeApp/src/
commonMain/ # Shared composables, ViewModels, navigation
kotlin/
composeResources/ # Shared images, fonts, strings
androidMain/ # Android entry point (Activity)
iosMain/ # iOS entry point (UIViewController wrapper)
desktopMain/ # Desktop entry point (main fun)
wasmJsMain/ # Browser entry point (main fun)
// androidMain — Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
// desktopMain — main function
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "MyApp",
state = rememberWindowState(width = 1200.dp, height = 800.dp)
) {
App()
}
}
// wasmJsMain — browser
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport(document.body!!) { App() }
}
// commonMain
@Composable
fun App() {
MaterialTheme {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("detail/{id}") { backStack ->
DetailScreen(backStack.arguments?.getString("id") ?: "")
}
}
}
}
Use expect/actual only when you need platform-specific UI behaviour.
// commonMain
@Composable
expect fun PlatformSpecificContent()
expect fun openUrl(url: String)
// androidMain
@Composable
actual fun PlatformSpecificContent() {
// Android-only widget
}
actual fun openUrl(url: String) {
// Use Android Intent
}
// desktopMain
@Composable
actual fun PlatformSpecificContent() {
// Desktop-specific panel
}
actual fun openUrl(url: String) {
Desktop.getDesktop().browse(URI(url))
}
// wasmJsMain
@Composable
actual fun PlatformSpecificContent() { /* no-op or web specific */ }
actual fun openUrl(url: String) {
window.open(url, "_blank")
}
Compose Multiplatform has a unified resource system. Place all shared assets in commonMain/composeResources/:
composeResources/
drawable/ # SVG/XML vector drawables
font/ # TTF/OTF fonts
values/
strings.xml # Localised strings
// Access resources in composables
import composeapp.composeresources.*
@Composable
fun Logo() {
Image(
painter = painterResource(Res.drawable.logo),
contentDescription = "Logo"
)
}
val label = stringResource(Res.string.welcome_message)
// desktopMain
fun main() = application {
val windowState = rememberWindowState(
placement = WindowPlacement.Maximized
)
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "MyApp",
icon = BitmapPainter(useResource("icon.png", ::loadImageBitmap))
) {
MenuBar {
Menu("File") {
Item("Open", onClick = { /* ... */ })
Separator()
Item("Exit", onClick = ::exitApplication)
}
}
App()
}
}
// wasmJsMain
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport(document.getElementById("root")!!) {
App()
}
}
wasmJs limitations:
external declarationscomposeResources/font/kotlinx.browser for DOM interop where needed// commonMain
val appModule = module {
viewModel { HomeViewModel(get()) }
viewModel { DetailViewModel(get()) }
}
@Composable
fun App() {
KoinApplication(application = {
modules(appModule, sharedModule)
}) {
MaterialTheme {
NavHost(/* ... */)
}
}
}
// In composables
@Composable
fun HomeScreen() {
val viewModel: HomeViewModel = koinViewModel()
// ...
}
# .github/workflows/build.yml
name: Build & Test
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
task: testDebugUnitTest # Android JVM
- os: macos-latest
task: iosSimulatorArm64Test # iOS simulator
- os: ubuntu-latest
task: desktopTest # Desktop JVM
- os: ubuntu-latest
task: wasmJsBrowserTest # Web/WASM
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v3
- run: ./gradlew ${{ matrix.task }}
# Android APK/AAB
./gradlew assembleRelease
./gradlew bundleRelease
# Desktop distributable
./gradlew packageDmg # macOS
./gradlew packageMsi # Windows
./gradlew packageDeb # Linux
# Web bundle
./gradlew wasmJsBrowserProductionWebpack
Mark ViewModel state as @Stable or use @Immutable to prevent unnecessary recompositions:
@Immutable
data class UiState(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// Prefer LazyColumn/LazyGrid over Column with forEach
LazyColumn {
items(items, key = { it.id }) { item ->
ItemCard(item)
}
}
val filteredItems by remember(query) {
derivedStateOf { allItems.filter { it.name.contains(query) } }
}
// commonMain — use Kamel for multiplatform async image loading
implementation(libs.kamel.image)
@Composable
fun AsyncImage(url: String) {
KamelImage(
resource = asyncPainterResource(url),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
commonMain — never platform-specific in shared UIcomposeResources/ — never raw platform asset folders@Immutable/@Stable on all state classes — prevents recomposition thrashingkey = {} in lazy layouts — always supply a stable keycommonMain composablesColumn { forEach } for long lists (use LazyColumn)key in items {} — causes full recomposition on list updatesdp and sp everywherecommonMain — keep it in platform source setsdata-ai
Use when adding AI-powered analytics to a SaaS platform — semantic search over business data, natural language queries, trend detection, anomaly alerts, and AI-generated insights for dashboards. Covers embeddings, NL2SQL, and per-tenant analytics...
data-ai
Design AI-powered analytics dashboards — what metrics to show, how to display AI predictions and confidence, drill-down patterns, KPI cards, trend visualisation, AI Insights panels, export design, and role-based dashboard variants. Invoke when...
development
Use when designing, building, reviewing, or upgrading production software systems that must be secure, performant, maintainable, scalable, and user-centered. Apply before writing specs, code, architecture, APIs, databases, mobile apps, SaaS platforms, or ERP systems.
development
Professional web app UI using commercial templates (Tabler/Bootstrap 5) with strong frontend design direction when needed. Use for CRUD interfaces, dashboards, admin panels with SweetAlert2, DataTables, Flatpickr. Clone seeder-page.php, use...