Kotlin Multiplatform Introduction
KMP is Kotlin's ultimate vision — Charlie's OrderValidator is written once and reused across JVM backend, iOS app, JS frontend, and Wasm browser. The expect/actual mechanism keeps platform differences only at the boundaries.
1. What You'll Learn
- KMP architecture:
commonMain/jvmMain/iosMain/jsMain - Shared module strategies: networking, data models, business logic
- Gradle multiplatform configuration
- Interoperability: JVM ↔ Java / iOS ↔ Swift / JS ↔ JavaScript
- Charlie in action:
OrderValidatorshared incommonMain
2. A Real Architect's Story
(1) Pain Point: Triplicating Business Logic Across Three Platforms
Charlie's company has a JVM backend, an iOS app, and a JS frontend. The same OrderValidator logic was implemented three times in Java, Swift, and TypeScript. Fixing one bug required changes in three places — this caused two inconsistent-behavior incidents in one month.
(2) KMP's Shared Code Solution
KOTLIN
// commonMain: ONE implementation for all platforms
class OrderValidator {
fun validate(order: Order): List<String> {
val errors = mutableListOf<String>()
if (!order.id.startsWith("ORD-")) errors.add("Invalid ID")
if (order.total < 0) errors.add("Negative total")
return errors
}
}
One codebase, three platforms. Fix a bug once, and behavior is naturally consistent everywhere.
3. KMP Architecture
(1) Project Structure
TEXT
shared/
├── src/
│ ├── commonMain/kotlin/ # Shared code (all platforms)
│ │ └── com/order/
│ │ ├── Order.kt
│ │ ├── OrderValidator.kt
│ │ └── Platform.kt # expect declarations
│ ├── commonTest/kotlin/ # Shared tests
│ ├── jvmMain/kotlin/ # JVM-specific code
│ │ └── com/order/
│ │ └── Platform.kt # actual for JVM
│ ├── iosMain/kotlin/ # iOS-specific code
│ │ └── com/order/
│ │ └── Platform.kt # actual for iOS
│ └── jsMain/kotlin/ # JS-specific code
│ └── com/order/
│ └── Platform.kt # actual for JS
└── build.gradle.kts
(2) expect / actual Mechanism
KOTLIN
// commonMain: declare what you need (expect)
expect fun getPlatformName(): String
expect class DateFormatter() {
fun format(timestamp: Long): String
}
// jvmMain: provide JVM implementation (actual)
actual fun getPlatformName(): String = "JVM"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String =
java.text.SimpleDateFormat("yyyy-MM-dd").format(timestamp)
}
// iosMain: provide iOS implementation (actual)
actual fun getPlatformName(): String = "iOS"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String {
// Use NSDateFormatter
return NSDateFormatter().apply {
dateFormat = "yyyy-MM-dd"
}.stringFromDate(NSDate(timestamp / 1000.0))
}
}
// jsMain: provide JS implementation (actual)
actual fun getPlatformName(): String = "JS"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String {
// Use JavaScript Date
return js("new Date(timestamp).toISOString().split('T')[0]")
}
}
(3) KMP Multiplatform Architecture
flowchart TD
A[commonMain<br/>Shared Business Logic] --> B[jvmMain<br/>JVM Actual]
A --> C[iosMain<br/>iOS Actual]
A --> D[jsMain<br/>JS Actual]
A --> E[wasmMain<br/>Wasm Actual]
B --> B1[Spring Boot<br/>Backend]
C --> C1[iOS App<br/>Swift Interop]
D --> D1[Node.js / Browser]
E --> E1[Web Assembly Runtime]
4. Gradle Multiplatform Configuration
(1) build.gradle.kts
KOTLIN
plugins {
kotlin("multiplatform") version "1.9.22"
}
group = "com.order"
version = "1.0.0"
repositories {
mavenCentral()
}
kotlin {
// Declare target platforms
jvm {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
js(IR) {
browser()
nodejs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting
val jvmTest by getting
val iosMain by getting
val jsMain by getting
}
}
5. Shared Module Strategy
(1) What to Share and What Not to Share
| Layer | Sharing Strategy | Reason |
|---|---|---|
| Data models | ✅ Fully shared | Pure data, no platform dependency |
| Business logic | ✅ Fully shared | Core rules must be consistent |
| Networking | ✅ Shared interface + expect HTTP | Interface unified, implementations differ |
| Serialization | ✅ Shared (kotlinx.serialization) | Multi-format native support |
| UI | ❌ Platform-specific | UI frameworks differ significantly |
| Database | ⚠️ Shared SQL / expect | SQL sharable, drivers differ |
| Logging | ⚠️ expect/actual | Platform logging APIs differ |
(2) Shared Strategy Patterns
KOTLIN
// Pattern 1: Pure shared code (no expect/actual needed)
data class Order(val id: String, val total: Double, val status: String)
class OrderValidator {
fun validate(order: Order): List<String> = buildList {
if (!order.id.startsWith("ORD-")) add("Invalid ID format")
if (order.total < 0) add("Negative total")
if (order.status !in validStatuses) add("Invalid status")
}
companion object {
private val validStatuses = setOf("PENDING", "CONFIRMED", "SHIPPED", "CANCELLED")
}
}
// Pattern 2: Interface + expect factory
interface HttpClient {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}
expect fun createHttpClient(): HttpClient
// Pattern 3: expect function for platform-specific behavior
expect fun logDebug(tag: String, message: String)
6. Interoperability
(1) Platform Interoperability Methods
| Platform Pair | Interoperability | Notes |
|---|---|---|
| JVM ↔ Java | Seamless bidirectional | Kotlin calls Java directly; Java can call Kotlin |
| iOS ↔ Swift | Bidirectional | Kotlin compiles to Obj-C framework; Swift calls seamlessly |
| JS ↔ JavaScript | Bidirectional | js() function calls JS; @JsExport exports Kotlin |
| Wasm ↔ JS | One-way (JS calls Kotlin) | Wasm module exports functions for JS calls |
(2) JVM Interoperability Example
KOTLIN
// Kotlin calling Java
val order = JavaOrderService() // Java class
order.processOrder("ORD-001") // Java method
// Java calling Kotlin (generated bytecode is standard)
// OrderKt.processOrder(order); // Top-level function
(3) JS Interoperability Example
KOTLIN
// Kotlin calling JavaScript
fun fetchFromApi(url: String): dynamic {
return js("fetch(url).then(r => r.json())")
}
// Export Kotlin to JavaScript
@JsExport
class OrderValidator {
fun validate(id: String, total: Double): Boolean {
return id.startsWith("ORD-") && total >= 0
}
}
7. Complete Example: Cross-Platform OrderValidator
KOTLIN
// ============================================
// OrderProcessor - KMP Shared Module
// Feature: Shared OrderValidator with platform logging
// ============================================
// --- commonMain ---
data class Order(val id: String, val total: Double, var status: String, val customer: String)
// expect: platform-specific declaration
expect fun logInfo(tag: String, message: String)
class OrderValidator {
fun validate(order: Order): ValidationResult {
val errors = mutableListOf<String>()
if (!order.id.startsWith("ORD-")) errors.add("Invalid ID format: ${order.id}")
if (order.total < 0) errors.add("Negative total: ${order.total}")
if (order.total > 1_000_000) errors.add("Total exceeds maximum: ${order.total}")
if (order.status !in VALID_STATUSES) errors.add("Invalid status: ${order.status}")
val result = if (errors.isEmpty()) ValidationResult.Valid else ValidationResult.Invalid(errors)
logInfo("OrderValidator", "Validated ${order.id}: $result")
return result
}
fun calculatePriority(order: Order): String = when {
order.total > 10_000 -> "HIGH"
order.total > 1_000 -> "MEDIUM"
else -> "LOW"
}
companion object {
private val VALID_STATUSES = setOf("PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED")
}
}
sealed class ValidationResult {
object Valid : ValidationResult()
data class Invalid(val errors: List<String>) : ValidationResult()
}
// --- jvmMain ---
// actual fun logInfo(tag: String, message: String) {
// println("[$tag] $message") // Or use SLF4J
// }
// --- iosMain ---
// actual fun logInfo(tag: String, message: String) {
// NSLog("$tag: $message")
// }
// --- jsMain ---
// actual fun logInfo(tag: String, message: String) {
// console.log("[$tag] $message")
// }
// --- Demo (JVM target) ---
fun main() {
// Simulate JVM actual
// actual fun logInfo(tag: String, message: String) = println("[$tag] $message")
val validator = OrderValidator()
val orders = listOf(
Order("ORD-001", 299.99, "PENDING", "Alice"),
Order("BAD-002", -50.0, "INVALID", "Bob"),
Order("ORD-003", 15_000.00, "CONFIRMED", "Charlie"),
Order("ORD-004", 2_000_000.00, "PENDING", "Dave")
)
println("=== KMP OrderValidator Demo ===")
orders.forEach { order ->
val result = validator.validate(order)
val priority = validator.calculatePriority(order)
when (result) {
is ValidationResult.Valid -> println(" ✅ ${order.id}: Valid (Priority: $priority)")
is ValidationResult.Invalid -> println(" ❌ ${order.id}: ${result.errors}")
}
}
}
Output:
TEXT
=== KMP OrderValidator Demo ===
✅ ORD-001: Valid (Priority: LOW)
❌ BAD-002: [Invalid ID format: BAD-002, Negative total: -50.0, Invalid status: INVALID]
✅ ORD-003: Valid (Priority: HIGH)
❌ ORD-004: [Total exceeds maximum: 2000000.0]
❓ FAQ
Q What's the difference between KMP and Flutter?
A KMP shares business logic (native UI per platform); Flutter shares UI (Skia rendering). KMP is more flexible (preserving native UI experience); Flutter is more unified (single UI). They can complement each other.
Q Is KMP production-ready?
A JVM and Android targets are fully stable; iOS target is stable; JS and Wasm targets are rapidly maturing. Companies like Netflix, VMware, and Cash App already use it at scale in production.
Q Can expect/actual be used for classes?
A Yes.
expect class declares a cross-platform interface; actual class provides platform implementations. Constructor and method signatures must match exactly.Q How should shared and platform modules be organized?
A Put shared logic in
commonMain; isolate platform differences with expect/actual at the boundary. Avoid platform-specific code in commonMain — use expect to abstract it away.Q Is KMP friendly to iOS developers?
A Very friendly. KMP compiles to an Obj-C framework that Swift calls seamlessly. iOS developers only need to care about the Swift side — they don't need to know Kotlin.
Q What's the build speed like for KMP?
A Multi-target compilation increases build time (each target compiles independently). Gradle incremental compilation and Kotlin compiler caching help mitigate this. In CI/CD, parallel target builds are recommended.
📖 Summary
- KMP core architecture:
commonMain(shared) + platform-specific source sets (jvmMain, iosMain, jsMain, ...) expectdeclares cross-platform requirements;actualprovides platform implementations- Data models and business logic are fully shared; UI and platform APIs use
expect/actual - Gradle
kotlin("multiplatform")declares multi-target builds - Platform interoperability: JVM↔Java seamless, iOS↔Swift via Obj-C framework, JS↔JS via
js()/@JsExport - KMP is not all-or-nothing — you can gradually adopt it by sharing only partial modules
📝 Exercises
- Beginner (⭐): Create a KMP project, define
OrderincommonMain, and use it in JVM and JS targets. Hint:kotlin("multiplatform")Gradle plugin - Intermediate (⭐⭐): Use
expect/actualto makegetPlatformName()return different values on JVM and JS. Hint:expect fun getPlatformName(): String+actualimplementations - Challenge (⭐⭐⭐): Implement a cross-platform HTTP client: define
HttpClientinterface incommonMain, implement withjava.net.URLinjvmMainand with thefetchAPI injsMain. Hint:expect fun createHttpClient(): HttpClient



