Kotlin Multiplatform KMP in Practice: 5 Core Patterns for Building Cross-Platform Apps with Compose
Kotlin Multiplatform: One Codebase, All Platforms
iOS uses Swift, Android uses Kotlin, Web uses TypeScript — cross-platform development costs remain high. Kotlin Multiplatform (KMP) lets you share business logic with the same Kotlin code, and share UI with Compose Multiplatform, writing once to run on Android, iOS, Desktop, and Web. In 2026, KMP has become one of the optimal solutions for cross-platform development.
This article covers 5 core patterns, guiding you through the full-chain implementation of expect/actual mechanism → shared business logic → Compose UI → platform APIs → production builds.
Core Concepts
| Concept | Description |
|---|---|
| KMP | Kotlin Multiplatform Platform, Kotlin's cross-platform solution |
| expect/actual | KMP platform difference declaration and implementation mechanism |
| Compose Multiplatform | Cross-platform UI framework based on Jetpack Compose |
| Shared Logic | Business logic code shared across platforms |
| Platform-Specific | Implementation code targeting specific platforms |
| Kotlin/Native | Kotlin backend compiling to native binaries |
| Kotlin/JS | Kotlin backend compiling to JavaScript |
| Interop | Interaction with platform native code (Swift/JS/C) |
Problem Analysis: 5 Major KMP Challenges
- expect/actual learning curve: The declaration-implementation separation pattern requires adaptation
- Compose Multiplatform maturity: iOS is still in Beta stage
- Platform difference handling: Significant API differences between iOS/Android/Desktop
- Poor debugging experience: Complex cross-platform debugging chain
- Complex build configuration: Steep learning curve for Gradle multiplatform configuration
Step-by-Step: 5 KMP Implementation Patterns
Pattern 1: KMP Project Structure and expect/actual
// commonMain/src/commonMain/kotlin/Platform.kt
expect class Platform() {
val name: String
}
fun getPlatform(): Platform {
return Platform()
}
// androidMain/src/androidMain/kotlin/Platform.kt
actual class Platform actual constructor() {
actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
// iosMain/src/iosMain/kotlin/Platform.kt
actual class Platform actual constructor() {
actual val name: String = "iOS ${platform.Foundation.NSProcessInfo.processInfo.operatingSystemVersionString}"
}
Pattern 2: Shared Business Logic Layer
// commonMain
class GreetingRepository {
private val platform = getPlatform()
fun greet(): String {
return "Hello from ${platform.name}!"
}
}
class UserViewModel {
private val repository = GreetingRepository()
private val _greeting = MutableStateFlow("")
val greeting: StateFlow<String> = _greeting.asStateFlow()
fun loadGreeting() {
_greeting.value = repository.greet()
}
}
Pattern 3: Compose Multiplatform 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
@Composable
fun App() {
var greeting by remember { mutableStateOf("Loading...") }
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = greeting,
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { greeting = GreetingRepository().greet() }) {
Text("Load Greeting")
}
}
}
}
}
}
Pattern 4: Platform-Specific API Calls
// commonMain
expect class DateTimeFormatter() {
fun format(timestamp: Long): String
}
// androidMain
actual class DateTimeFormatter actual constructor() {
private val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
actual fun format(timestamp: Long): String = sdf.format(Date(timestamp))
}
// iosMain
actual class DateTimeFormatter actual constructor() {
actual fun format(timestamp: Long): String {
val date = NSDate(timeIntervalSince1970 = timestamp / 1000.0)
val formatter = NSDateFormatter().apply {
dateFormat = "yyyy-MM-dd HH:mm:ss"
}
return formatter.stringFromDate(date)
}
}
Pattern 5: Production Build and Release
// build.gradle.kts
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
jvm("desktop")
js(IR) {
browser()
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}
androidMain.dependencies {
implementation("androidx.activity:activity-compose:1.9.0")
}
}
}
Pitfall Guide
Pitfall 1: Referencing platform-specific APIs in commonMain
// ❌ Wrong: directly using Android API in commonMain
fun getPath() = android.os.Environment.getDataDirectory().path
// ✅ Correct: declare through expect/actual
// commonMain
expect fun getDataPath(): String
// androidMain
actual fun getDataPath() = android.os.Environment.getDataDirectory().path
Pitfall 2: Compose iOS memory leaks
// ❌ Wrong: not releasing iOS native references
class IOSBridge {
val delegate = object : NSURLSessionDelegate() { ... }
}
// ✅ Correct: use WeakReference or manual cleanup
class IOSBridge {
private var delegateRef: WeakReference<NSURLSessionDelegate>? = null
fun cleanup() { delegateRef = null }
}
Pitfall 3: Kotlin/Native concurrency restrictions
// ❌ Wrong: sharing mutable state across threads
var counter = 0 // crashes when accessed from different threads
// ✅ Correct: use AtomicReference
val counter = AtomicReference(0)
Pitfall 4: Overly complex Gradle configuration
// ❌ Wrong: all configuration in one build.gradle.kts
// 300+ line Gradle file
// ✅ Correct: split using convention plugins
// build-logic/convention/src/main/kotlin/KmpConventionPlugin.kt
class KmpConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.kotlin { /* standard KMP configuration */ }
}
}
Pitfall 5: Not handling platform font differences
// ❌ Wrong: using platform-specific font names
Text(fontFamily = FontFamily("SF Pro"))
// ✅ Correct: use Compose built-in fonts or conditional logic
val fontFamily = if (Platform.isIOS) FontFamily("SF Pro") else FontFamily("Roboto")
Error Troubleshooting
| # | Error | Cause | Solution |
|---|---|---|---|
| 1 | Unresolved reference: expect |
KMP plugin not enabled | Add kotlin("multiplatform") plugin |
| 2 | Actual class has no corresponding expect |
expect/actual mismatch | Check commonMain and platform Main declarations |
| 3 | Linker error iOS |
Kotlin/Native C interop issue | Check cinterop configuration and .def files |
| 4 | Compose compiler error |
Compose version incompatibility | Unify compose plugin and compiler versions |
| 5 | Gradle sync failed |
Multiplatform dependency conflict | Use version catalog for unified versions |
| 6 | JS compilation error |
Kotlin/JS doesn't support reflection | Avoid reflection APIs in commonMain |
| 7 | iOS build timeout |
Slow Kotlin/Native compilation | Enable incremental compilation and caching |
| 8 | ClassNotFoundException desktop |
JVM module misconfigured | Check jvm() target and mainClass |
| 9 | Module not found |
sourceSets path error | Verify src/commonMain/kotlin directory structure |
| 10 | SDK not found |
iOS SDK not installed | Install Xcode and Command Line Tools |
Comparison
| Dimension | KMP | Flutter | React Native | .NET MAUI |
|---|---|---|---|---|
| Language | Kotlin | Dart | JavaScript/TS | C# |
| UI Sharing | Compose | Widget | JSX | XAML |
| Native Performance | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Ecosystem Richness | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Learning Curve | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Native Interop | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| iOS Maturity | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
Summary: Kotlin Multiplatform offers unique flexibility in cross-platform development through its expect/actual mechanism and Compose Multiplatform. KMP suits teams already in the Kotlin/Android ecosystem, enabling incremental code sharing rather than full rewrites. With Compose Multiplatform iOS maturing in 2026, KMP is becoming the pragmatic choice for cross-platform development.
Online Tools
- JSON Formatter: /en/json/format
- Hash Calculator: /en/encode/hash
- cURL to Code: /en/dev/curl-to-code
Try these browser-local tools — no sign-up required →