| name | compose-multiplatform |
| description | Sharing UI with Compose Multiplatform 1.7+ across Android, iOS, Desktop (JVM), and Web (wasmJs). Covers module layout, entry points per platform, lifecycle, and when UI-per-platform is still better. Use when building shared screens. |
Compose Multiplatform
Instructions
Compose Multiplatform (CMP) lets you write one @Composable tree that renders on Android (via Jetpack Compose), iOS (via Skia/Metal), Desktop, and Web. It is stable for Android/Desktop, stable for iOS (1.7+), beta for wasmJs.
1. Module & plugin setup
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
androidTarget()
jvm("desktop")
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach {
it.binaries.framework { baseName = "Shared"; isStatic = true }
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
}
}
}
2. A shared screen
@Composable
fun App(viewModel: CounterViewModel) {
val state by viewModel.state.collectAsState()
MaterialTheme {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Count: ${state.count}", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(16.dp))
Button(onClick = viewModel::increment) { Text("Increment") }
}
}
}
3. Android entry point
class MainActivity : ComponentActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App(viewModel) }
}
}
4. iOS entry point
fun mainViewController(viewModel: CounterViewModel): UIViewController =
ComposeUIViewController { App(viewModel) }
import SwiftUI
import Shared
struct ContentView: View {
let viewModel: CounterViewModel
var body: some View {
ComposeView(viewModel: viewModel).ignoresSafeArea(.keyboard)
}
}
struct ComposeView: UIViewControllerRepresentable {
let viewModel: CounterViewModel
func makeUIViewController(context: Context) -> UIViewController {
Shared_iosKt.mainViewController(viewModel: viewModel)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
5. Desktop entry point
fun main() = application {
val viewModel = remember { CounterViewModel() }
Window(onCloseRequest = ::exitApplication, title = "My App") { App(viewModel) }
}
6. Lifecycle & view-models
Use the multiplatform androidx.lifecycle:lifecycle-viewmodel-compose (stable for KMP since 2.8). ViewModel and viewModelScope now exist in commonMain:
class CounterViewModel : ViewModel() {
private val _state = MutableStateFlow(CounterState())
val state: StateFlow<CounterState> = _state.asStateFlow()
fun increment() = viewModelScope.launch {
_state.update { it.copy(count = it.count + 1) }
}
}
7. When UI-per-platform is better
Share UI when the screens should look and behave identically and the feature set is within Compose's reach. Keep UI per platform when:
- You need fully native widgets (SwiftUI
List with swipe actions, Android AppWidget, system share sheets).
- Accessibility / haptics / gesture nuances must match platform conventions exactly.
- Integrating deeply with platform navigation (SwiftUI
NavigationStack backed by UIKit transitions).
In those cases keep presenters (StateFlow-emitting view-models) in commonMain and write native UI per target.
Checklist