Compose Multiplatform: One Codebase Everywhere
TOP 5 Feb. 8, 2026, 11:30 a.m.

Compose Multiplatform: One Codebase Everywhere

Imagine writing a single UI once and watching it run flawlessly on Android, iOS, desktop, and even the web. That’s the promise of Compose Multiplatform – a modern, declarative UI toolkit that lets you share code across the entire ecosystem without sacrificing native look‑and‑feel. In this article we’ll explore the core concepts, walk through a real project, and uncover pro tips that will help you ship a truly cross‑platform product faster.

What Compose Multiplatform Actually Is

Compose Multiplatform is an extension of Jetpack Compose, Google’s declarative UI framework for Android, that has been opened up to run on Kotlin/Native and Kotlin/JS targets. Under the hood it uses the same composable functions, state handling, and layout engine, but the rendering backend swaps out depending on the platform – Skia for desktop, UIKit for iOS, and Canvas/WebGL for the browser.

Because the UI layer is written in pure Kotlin, you can also place business logic, data models, and even networking code in a shared module. The result is a single source of truth for UI and core functionality, dramatically reducing duplication and maintenance overhead.

Setting Up the Development Environment

Before you can start coding, you need a compatible toolchain. The recommended stack includes:

  • IntelliJ IDEA Ultimate (or Community with the Kotlin Multiplatform plugin)
  • JDK 17 or newer
  • Android SDK for Android targets
  • Xcode (for iOS) – only required if you plan to build and test on a Mac
  • Gradle 8.x

Once the IDE is ready, create a new Gradle project and enable the org.jetbrains.compose plugin. Below is the minimal settings.gradle.kts and build.gradle.kts configuration that supports Android, iOS, JVM desktop, and JS.

rootProject.name = "ComposeMultiplatformDemo"

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
plugins {
    kotlin("multiplatform") version "2.0.0"
    id("org.jetbrains.compose") version "1.5.0"
    id("com.android.application") version "8.1.0"
}

kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    jvm("desktop")
    js(IR) {
        browser()
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material3)
            }
        }
        val androidMain by getting {
            dependencies {
                implementation(compose.uiTooling)
                implementation("androidx.activity:activity-compose:1.8.0")
            }
        }
        val iosMain by creating {
            dependencies {
                // iOS‑specific libraries can be added here
            }
        }
        val desktopMain by getting {
            dependencies {
                implementation(compose.desktop.currentOs)
            }
        }
        val jsMain by getting {
            dependencies {
                implementation(compose.web.core)
            }
        }
    }
}

android {
    compileSdk = 34
    defaultConfig {
        applicationId = "com.codeyaan.composemultiplatform"
        minSdk = 21
        targetSdk = 34
    }
}

With the Gradle files in place, sync the project. IntelliJ will automatically generate platform‑specific source sets (e.g., androidMain, iosMain, desktopMain), giving you a clean separation between shared and native code.

Building Your First Shared UI

The heart of Compose Multiplatform is the composable function. Let’s start with a simple “Hello, World!” that works everywhere. Create a file Greeting.kt inside commonMain/kotlin/com/codeyaan/ui and add the following code:

package com.codeyaan.ui

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun Greeting(name: String) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        elevation = CardDefaults.cardElevation(8.dp)
    ) {
        Box(
            modifier = Modifier
                .padding(24.dp),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Hello, $name!", style = MaterialTheme.typography.headlineMedium)
        }
    }
}

This snippet uses only compose.runtime and compose.material3, both of which are available on every target. The UI is declarative, so the same code renders as a native Android view hierarchy, a SwiftUI‑compatible view on iOS, and an HTML canvas on the web.

Hooking the Shared UI into Android

In androidMain, replace the default MainActivity content with the shared Greeting composable:

package com.codeyaan.android

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.codeyaan.ui.Greeting
import androidx.compose.material3.MaterialTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting(name = "Android")
            }
        }
    }
}

Run the app on an emulator – you’ll see the card rendered with the native Android Material theme.

Running the Same UI on Desktop

For the desktop target, create an entry point in desktopMain/kotlin/com/codeyaan/desktop:

package com.codeyaan.desktop

import androidx.compose.ui.window.application
import com.codeyaan.ui.Greeting
import androidx.compose.material3.MaterialTheme

fun main() = application {
    Window(onCloseRequest = ::exitApplication, title = "Compose Multiplatform Demo") {
        MaterialTheme {
            Greeting(name = "Desktop")
        }
    }
}

Execute ./gradlew :desktopRun and you’ll get a native window with the exact same card, proving the UI truly is shared.

Sharing Business Logic Across Platforms

One of the biggest wins of a multiplatform approach is the ability to write domain logic once. Let’s add a simple counter that increments every time the user taps a button. Place the following file in commonMain/kotlin/com/codeyaan/domain/Counter.kt:

package com.codeyaan.domain

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class Counter {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count

    fun increment() {
        _count.value += 1
    }
}

The Counter class uses Kotlin’s StateFlow to expose observable state. Because StateFlow is part of Kotlin’s standard library, it works on all platforms without extra dependencies.

Now, create a UI that consumes this shared logic. Add a new composable in commonMain/kotlin/com/codeyaan/ui/CounterScreen.kt:

package com.codeyaan.ui

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.codeyaan.domain.Counter
import kotlinx.coroutines.flow.collectAsState

@Composable
fun CounterScreen(counter: Counter) {
    val count by counter.count.collectAsState()
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "You clicked $count times", style = MaterialTheme.typography.titleLarge)
        Spacer(modifier = Modifier.height(24.dp))
        Button(onClick = { counter.increment() }) {
            Text("Increment")
        }
    }
}

Notice how the UI remains oblivious to the platform – it simply observes the shared StateFlow and triggers a method call. You can now reuse CounterScreen on Android, iOS, desktop, and web with a single line of platform‑specific glue code.

Platform‑Specific Integrations

Even though most of your code lives in the shared module, you’ll inevitably need to call native APIs – for example, accessing the camera on Android or handling deep links on iOS. Compose Multiplatform makes this straightforward via expect/actual declarations.

Example: Opening a URL in the System Browser

First, define an expected function in commonMain/kotlin/com/codeyaan/util/PlatformUtils.kt:

package com.codeyaan.util

expect fun openUrl(url: String)

Then provide platform‑specific implementations.

Android actual:

package com.codeyaan.util

import android.content.Intent
import android.net.Uri
import androidx.compose.ui.platform.LocalContext

actual fun openUrl(url: String) {
    val context = LocalContext.current
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    context.startActivity(intent)
}

iOS actual (using Kotlin/Native interop):

package com.codeyaan.util

import platform.UIKit.UIApplication
import platform.Foundation.NSURL

actual fun openUrl(url: String) {
    val nsUrl = NSURL.URLWithString(url) ?: return
    UIApplication.sharedApplication.openURL(nsUrl)
}

Now any composable can call openUrl("https://codeyaan.com") and the appropriate native mechanism will be invoked.

Testing the Shared Code

Testing is a critical part of any production‑grade project. Because your business logic lives in commonMain, you can write unit tests once and run them on the JVM, which is fast and reliable.

Create commonTest/kotlin/com/codeyaan/domain/CounterTest.kt:

package com.codeyaan.domain

import kotlin.test.Test
import kotlin.test.assertEquals

class CounterTest {
    @Test
    fun testIncrement() {
        val counter = Counter()
        assertEquals(0, counter.count.value)
        counter.increment()
        assertEquals(1, counter.count.value)
        counter.increment()
        assertEquals(2, counter.count.value)
    }
}

Run ./gradlew allTests and you’ll see the test executed on the JVM, guaranteeing that your core logic works before you even launch a platform emulator.

Pro tip: Use kotlinx.coroutines.test to control time‑based flows and avoid flaky tests when dealing with asynchronous state.

Performance Considerations

While Compose Multiplatform abstracts away the rendering backend, it’s still important to be mindful of performance nuances on each platform.

  • Avoid heavy recompositions: Keep composables small and use remember or derivedStateOf to cache expensive calculations.
  • Leverage platform‑specific lazy lists: Use LazyColumn for long lists; on the web it maps to virtual scrolling, while on iOS it translates to a native UITableView.
  • Image loading: The shared Image composable works, but you may want to plug in a platform‑optimized loader (Coil on Android, SDWebImage on iOS) via expect/actual to avoid unnecessary memory overhead.

Profiling tools differ – Android Studio’s profiler for Android, Instruments for iOS, and Chrome DevTools for the web – but the same Compose UI tree can be inspected across them, making performance debugging a unified experience.

Real‑World Use Cases

1. Productivity Apps: Companies like JetBrains already ship Compose for Desktop and are experimenting with multiplatform extensions for their IDE plugins. A single UI can serve Windows, macOS, and Linux without maintaining three separate codebases.

2. IoT Dashboards: Imagine a smart‑home hub that runs on a Raspberry Pi (JVM), displays a web UI for remote access, and also provides a native Android companion. Compose Multiplatform lets you reuse the same dashboard components everywhere.

3. Education Platforms: For a service like Codeyaan, you can build interactive coding exercises that work on a student’s phone, tablet, or laptop, ensuring a consistent learning experience while reducing development effort.

Advanced Tips & Tricks

Pro tip #1 – Use Gradle’s buildConfig to inject API keys per platform. This keeps secrets out of the shared source while allowing each platform to access its own configuration.
Pro tip #2 – Adopt a “shared UI first” mindset. Start by designing composables in commonMain, then only fall back to platform‑specific code when you truly need a native feature.
Pro tip #3 – Enable incremental compilation. Add kotlin.incremental=true to gradle.properties to speed up rebuilds, especially when iterating on UI changes.

Deploying to the App Stores

Deploying a Compose Multiplatform app follows the same steps as native apps. For Android, generate an APK or AAB with ./gradlew assembleRelease. For iOS, Xcode will pick up the generated framework from the Gradle build – just add it as a dependency in your Xcode project and configure a standard Info.plist.

Desktop apps can be packaged with compose.desktop.application plugin, producing installers for Windows (.msi), macOS (.dmg), and Linux (.deb). Web targets are built with jsBrowserDistribution, which outputs static assets ready for any CDN.

Conclusion

Compose Multiplatform empowers you to write truly unified applications with a single Kotlin codebase, dramatically cutting down on duplication while preserving native performance and look‑and‑feel. By mastering shared UI, expect/actual interop, and platform‑agnostic testing, you can ship products that run everywhere – from phones to laptops, from browsers to embedded devices. The ecosystem is still evolving, but the core concepts are solid, and early adopters are already reaping the benefits of faster iteration cycles and lower maintenance costs. Dive in, experiment with the snippets above, and watch your code travel across the entire device landscape with ease.

Share this article