Skip to content
Blog

Compose Multiplatform for Shared UI

Learn how to share a single Kotlin UI across Android, iOS, desktop, and web with Compose Multiplatform, JetBrains declarative UI framework.

Published on August 17, 2026

AI Assistant

Maintaining two native UIs — a Kotlin Android app and a SwiftUI iOS app — means every feature ships twice. Compose Multiplatform (CMP) is a declarative UI framework for Kotlin that lets you write that UI once and run it on Android, iOS, desktop (Windows, macOS, Linux), and the web, all from a single codebase. It is built on top of Jetpack Compose, so Android developers feel right at home, while Kotlin Multiplatform gives you access to native APIs when you need them.

In this tutorial, you will learn how to scaffold a Compose Multiplatform project, share UI between Android and iOS, and add desktop and web targets. Key technologies: Kotlin Multiplatform, Jetpack Compose, Gradle, and the composeApp module structure.

Prerequisites

  • JDK 17+
  • Android Studio (with the Compose Multiplatform and Kotlin Multiplatform plugins)
  • Kotlin 2.0+ and Gradle 8+
  • Basic familiarity with Kotlin and declarative UI concepts

Core Content

Scaffold the project

Compose Multiplatform projects use a standard Kotlin Multiplatform layout with a shared composeApp module. The easiest way to start is the project wizard at jb.gg/start-cmp, which generates this structure:

composeApp/
  src/
    commonMain/kotlin/    # Shared UI + logic for all targets
    androidMain/kotlin/   # Android-specific code
    iosMain/kotlin/       # iOS-specific code
    desktopMain/kotlin/   # Desktop entry point
    wasmJsMain/kotlin/    # Web (Wasm) entry point
  build.gradle.kts
gradle/
  libs.versions.toml

The commonMain source set is where the magic happens — every Composable you put there is available on every platform.

Define targets in the build file

The composeApp/build.gradle.kts declares which platforms you want to target. For Android + iOS + Desktop + Web:

kotlin {
    androidTarget()
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { it.binaries.framework {
        baseName = "ComposeApp"
        isStatic = true
    } }

    jvm("desktop")
    wasmJs {
        moduleName = "composeApp"
        browser()
    }

    sourceSets {
        val desktopMain by getting
        androidMain.dependencies {
            implementation(compose.preview)
        }
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
        }
    }
}

Each target compiles the shared commonMain code and links it against its native runtime — Android uses Jetpack Compose, iOS uses Skia with a native shell, and desktop targets the JVM.

Write shared UI

Here’s a shared screen that works everywhere. Notice it’s just regular Jetpack Compose — no platform-specific code:

@Composable
fun GreetingScreen(name: String, onIncrement: () -> Unit, count: Int) {
    MaterialTheme {
        Column(
            modifier = Modifier.fillMaxSize().padding(24.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(text = "Hello, $name!", style = MaterialTheme.typography.headlineMedium)
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = onIncrement) {
                Text("Count is $count")
            }
        }
    }
}

The UI state is driven by callbacks so the same screen is fully testable and reusable across targets.

Platform entry points

Each platform needs a thin entry point that launches the shared UI. On Android it’s an Activity:

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

On iOS, a MainViewController wraps the same Composable:

fun MainViewController() = ComposeUIViewController { App() }

And on desktop, a main function with the application DSL:

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        title = "Compose Multiplatform",
        state = rememberWindowState(width = 800.dp, height = 600.dp)
    ) {
        App()
    }
}

Calling native APIs from shared code

Because Compose Multiplatform is built on Kotlin Multiplatform, you can access platform capabilities through expect/actual. For example, reading a platform identifier:

// commonMain
expect fun platformName(): String

// androidMain
actual fun platformName(): String = "Android: ${Build.VERSION.SDK_INT}"

// iosMain
actual fun platformName(): String = "iOS: ${UIDevice.currentDevice.systemName}"

Putting It All Together

The complete project runs the identical App() Composable on Android, iOS, desktop, and web. Compose Multiplatform shares most of its API with Jetpack Compose, so the Android experience is identical to standard Jetpack Compose development, while iOS, desktop, and Wasm targets get the same declarative code compiled natively. Desktop apps are JVM-based with hardware-accelerated rendering, and web support (in Beta since the 1.9.0 release) targets Kotlin/Wasm for predictable performance in the browser.

Conclusion & Next Steps

You’ve scaffolded a Compose Multiplatform project, written shared UI in commonMain, added per-platform entry points, and learned how to access native APIs with expect/actual.

Next Steps: try sharing an entire app between iOS and Android, embed native views like MKMapView via interop, and explore the official samples for navigation, state management, and material 3 theming across all four targets.

References: