Claude
Skills
Sign in
Back

compose-multiplatform

Included with Lifetime
$97 forever

Compose Multiplatform (CMP) by JetBrains — declarative UI framework that runs on Android, iOS, JVM Desktop, and Web (Wasm). Built on top of Jetpack Compose. Covers @Composable functions, state hoisting, side effects, navigation (Voyager, Decompose), Material 3, theming, and platform-specific UI bridging on iOS. USE WHEN: user mentions "Compose Multiplatform", "@Composable", "remember", "MutableState", "Compose iOS", "Compose Desktop", "Compose Wasm", "JetBrains Compose", "Voyager", "Decompose", "compose-multiplatform-resources" DO NOT USE FOR: KMP module setup (gradle, expect/actual) - use `mobile/kotlin-multiplatform` DO NOT USE FOR: Jetpack Compose Android-only - use `mobile/jetpack-compose` DO NOT USE FOR: SwiftUI native - use `mobile/ios-native` or `languages/swift` DO NOT USE FOR: Pure Kotlin language - use `languages/kotlin`

Design

What this skill does

# Compose Multiplatform

> **References**: [ios-platform.md](quick-ref/ios-platform.md) for iOS-specific patterns (UIViewController interop, gesture differences, native scroll). [navigation-di.md](quick-ref/navigation-di.md) for navigation libraries (Voyager, Decompose) and DI integration. [theming.md](quick-ref/theming.md) for Material 3 + custom design systems, dark mode, dynamic color.
>
> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `compose-multiplatform`.

## What CMP Is

Compose Multiplatform brings Jetpack Compose to **Android, iOS, JVM Desktop, and Web (Wasm)**. Same `@Composable` code renders on every target via Skia.

**Status (Mid-2026)**:
- Android: Stable (it's Jetpack Compose underneath)
- Desktop: Stable
- iOS: **Stable since 1.8.0** (early 2025)
- Web (Wasm): Beta — usable for prototypes, some gaps

**Trade-offs vs native**:
- ✅ Single UI codebase, fast iteration
- ✅ Pixel-perfect parity, custom design systems easy
- ⚠️ iOS gestures, accessibility, system fonts, share sheets need bridging
- ⚠️ Bundle size larger than pure SwiftUI (Skia ~5-8MB)
- ⚠️ iOS Core Animation transitions don't apply automatically

## Minimal Setup

```kotlin
// shared/build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.2.0"
    id("com.android.library") version "8.7.0"
    id("org.jetbrains.compose") version "1.8.0"
    kotlin("plugin.compose") version "2.2.0"   // Required for Kotlin 2.x
}

kotlin {
    androidTarget()
    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"
            isStatic = true
        }
    }
    jvm("desktop")

    sourceSets {
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
            implementation(compose.components.resources)
            implementation("org.jetbrains.compose.material:material-icons-extended:1.8.0")
        }
        androidMain.dependencies {
            implementation("androidx.activity:activity-compose:1.10.0")
        }
        desktopMain.dependencies {
            implementation(compose.desktop.currentOs)
        }
    }
}

compose.resources {
    publicResClass = true
    packageOfResClass = "com.example.shared.resources"
}
```

## First Composable

```kotlin
// shared/src/commonMain/kotlin/App.kt
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun App() {
    MaterialTheme {
        var count by remember { mutableStateOf(0) }
        Column(
            modifier = Modifier.fillMaxSize().padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            Text("Count: $count", style = MaterialTheme.typography.headlineMedium)
            Button(onClick = { count++ }) {
                Text("Increment")
            }
        }
    }
}
```

## Wiring Per Platform

### Android

```kotlin
// apps/android/MainActivity.kt
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { App() }
    }
}
```

### iOS

```kotlin
// shared/src/iosMain/kotlin/MainViewController.kt
import androidx.compose.ui.window.ComposeUIViewController

fun MainViewController() = ComposeUIViewController { App() }
```

```swift
// apps/ios/ContentView.swift
import SwiftUI
import Shared

struct ContentView: View {
    var body: some View {
        ComposeView()
            .ignoresSafeArea(.keyboard)
    }
}

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        Main_iosKt.MainViewController()
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
```

### Desktop

```kotlin
// apps/desktop/Main.kt
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication, title = "BHODL") {
        App()
    }
}
```

## State

### `remember` and `mutableStateOf`

```kotlin
@Composable
fun NameField() {
    var name by remember { mutableStateOf("") }
    OutlinedTextField(
        value = name,
        onValueChange = { name = it },
        label = { Text("Name") },
    )
}
```

`remember` survives recomposition; lost on configuration change/process death. Use `rememberSaveable` to persist across config changes.

```kotlin
var name by rememberSaveable { mutableStateOf("") }
```

### State Hoisting

Lift state to the lowest common ancestor. Children become stateless and reusable.

```kotlin
@Composable
fun NameForm(
    name: String,
    onNameChange: (String) -> Unit,
    onSubmit: () -> Unit,
) {
    Column {
        OutlinedTextField(value = name, onValueChange = onNameChange)
        Button(onClick = onSubmit) { Text("Submit") }
    }
}

@Composable
fun ParentScreen() {
    var name by remember { mutableStateOf("") }
    NameForm(
        name = name,
        onNameChange = { name = it },
        onSubmit = { /* ... */ },
    )
}
```

### Collecting Flows

```kotlin
@Composable
fun WalletScreen(viewModel: WalletViewModel) {
    val state by viewModel.state.collectAsState()

    when (val s = state) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> WalletList(s.wallets)
        is UiState.Error -> ErrorView(s.message)
    }
}
```

`collectAsState` works on all CMP targets (uses platform-appropriate Flow collection).

## Side Effects

| Effect | Use case |
|---|---|
| `LaunchedEffect(key1, ...)` | Run suspending work; cancels on key change |
| `DisposableEffect(key)` | Setup with cleanup (listeners) |
| `SideEffect { }` | Run on every successful recomposition (rare) |
| `produceState` | Convert non-Compose source to State |
| `derivedStateOf` | Memoize computation depending on multiple states |
| `rememberCoroutineScope()` | Launch coroutines tied to composable lifecycle |
| `snapshotFlow { ... }` | Convert State to Flow |

```kotlin
@Composable
fun BalanceObserver(walletId: String, repo: WalletRepository) {
    val balance by produceState<Balance?>(initialValue = null, walletId) {
        value = repo.getBalance(walletId)
    }
    Text("Balance: ${balance?.confirmed ?: "..."}")
}

@Composable
fun TickerExample() {
    var seconds by remember { mutableStateOf(0) }
    LaunchedEffect(Unit) {
        while (true) {
            delay(1000)
            seconds++
        }
    }
    Text("Elapsed: $seconds")
}

@Composable
fun ScrollPositionTracker(scrollState: ScrollState) {
    LaunchedEffect(scrollState) {
        snapshotFlow { scrollState.value }
            .collect { offset -> log("scroll: $offset") }
    }
}
```

## Common UI Patterns

### Lists

```kotlin
@Composable
fun WalletList(wallets: List<Wallet>) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        items(wallets, key = { it.id }) { wallet ->
            WalletItem(wallet)
        }
    }
}
```

`key = { it.id }` enables Compose to track items across reorders/animations.

### Forms

```kotlin
@Composable
fun SendForm(onSend: (String, Long) -> Unit) {
    var address by remember { mutableStateOf("") }
    var amount by remember { mutableStateOf("") }
    val canSend = address.isNotBlank() && amount.toLongOrNull() != null

    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        OutlinedTextField(
            value = address,
            onValueChange = { address = it },
            label = { Text("Address") },
            singleLine = true,
        )
        OutlinedTextField(
     

Related in Design