Kotlin Multiplatform Mobile: Sharing Code Across iOS and Android
Kotlin Multiplatform (KMP) lets you write shared business logic once and run it on both iOS and Android while keeping native UI on each platform. Unlike Flutter or React Native which replace native UI frameworks, KMP shares only the code that should be shared — networking, data processing, state management, business rules — and lets each platform handle its own UI with SwiftUI or Jetpack Compose. This pragmatic approach has made KMP the fastest-growing cross-platform solution since Google officially endorsed it in 2024.
Why KMP Over Flutter or React Native
The key difference is philosophy. Flutter and React Native own the entire stack — UI, navigation, gestures, animations. This means you’re always one step behind native platform features and fighting framework limitations for platform-specific behavior. KMP takes the opposite approach: share what’s genuinely shareable (networking, persistence, validation, analytics), keep what’s platform-specific native (UI, permissions, hardware access).
Furthermore, KMP doesn’t require your entire team to adopt a new framework. iOS developers continue writing SwiftUI, Android developers continue writing Jetpack Compose. The shared module is just a library that both platforms consume. This makes adoption gradual — you can start with one shared module and expand over time.
Project Structure and Setup
// build.gradle.kts (shared module)
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:2.3.8")
implementation("io.ktor:ktor-client-content-negotiation:2.3.8")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8")
implementation("app.cash.sqldelight:runtime:2.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.8")
implementation("app.cash.sqldelight:android-driver:2.0.1")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.8")
implementation("app.cash.sqldelight:native-driver:2.0.1")
}
}
}Kotlin Multiplatform: The Expect/Actual Pattern
When shared code needs platform-specific implementations, KMP uses the expect/actual mechanism. You declare an expected interface in common code, then provide actual implementations for each platform.
// commonMain - Declare expected platform behavior
expect class PlatformContext
expect fun createDatabaseDriver(context: PlatformContext): SqlDriver
expect fun getDeviceInfo(): DeviceInfo
data class DeviceInfo(
val platform: String,
val osVersion: String,
val deviceModel: String,
val appVersion: String
)
// androidMain - Android implementation
actual typealias PlatformContext = android.content.Context
actual fun createDatabaseDriver(context: PlatformContext): SqlDriver {
return AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
actual fun getDeviceInfo(): DeviceInfo = DeviceInfo(
platform = "Android",
osVersion = "Android ${android.os.Build.VERSION.RELEASE}",
deviceModel = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}",
appVersion = BuildConfig.VERSION_NAME
)
// iosMain - iOS implementation
actual class PlatformContext // No-op on iOS
actual fun createDatabaseDriver(context: PlatformContext): SqlDriver {
return NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
actual fun getDeviceInfo(): DeviceInfo {
val device = UIDevice.currentDevice
return DeviceInfo(
platform = "iOS",
osVersion = "iOS ${device.systemVersion}",
deviceModel = device.model,
appVersion = NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as String
)
}Networking with Ktor Client
Ktor is the recommended HTTP client for KMP. It provides a common API across platforms while using platform-native engines underneath (OkHttp on Android, URLSession on iOS).
// commonMain - Shared API client
class ApiClient(private val httpClient: HttpClient) {
suspend fun getProducts(): List<Product> {
return httpClient.get("https://api.example.com/products")
.body()
}
suspend fun createOrder(order: OrderRequest): OrderResponse {
return httpClient.post("https://api.example.com/orders") {
contentType(ContentType.Application.Json)
setBody(order)
}.body()
}
companion object {
fun create(): ApiClient {
val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = false
})
}
install(HttpTimeout) {
requestTimeoutMillis = 30_000
connectTimeoutMillis = 10_000
}
install(Logging) {
level = LogLevel.HEADERS
}
}
return ApiClient(client)
}
}
}Persistence with SQLDelight
SQLDelight generates type-safe Kotlin APIs from SQL statements. It works across all KMP targets and provides compile-time verification of your queries.
-- shared/src/commonMain/sqldelight/com/example/ProductQueries.sq
CREATE TABLE Product (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
category TEXT NOT NULL,
inStock INTEGER AS Boolean NOT NULL DEFAULT 1,
lastUpdated TEXT NOT NULL
);
getAll:
SELECT * FROM Product ORDER BY name;
getByCategory:
SELECT * FROM Product WHERE category = ? AND inStock = 1;
search:
SELECT * FROM Product WHERE name LIKE '%' || ? || '%';
upsert:
INSERT OR REPLACE INTO Product(id, name, price, category, inStock, lastUpdated)
VALUES (?, ?, ?, ?, ?, ?);Migration Strategy: Native to KMP
The recommended migration path: start with a new shared module containing one feature (usually networking or analytics). Keep all existing native code. Gradually move business logic to shared code over multiple releases. Never migrate UI — keep it native. This approach de-risks the migration and lets teams build confidence with KMP before committing fully. Most teams report 30-40% code sharing after the first phase and 50-60% after fully migrating business logic.
Key Takeaways
For further reading, refer to the Android developer docs and the Apple developer docs for comprehensive reference material.
Key Takeaways
- Start with a solid foundation and build incrementally based on your requirements
- Test thoroughly in staging before deploying to production environments
- Monitor performance metrics and iterate based on real-world data
- Follow security best practices and keep dependencies up to date
- Document architectural decisions for future team members
Kotlin Multiplatform offers the most pragmatic approach to cross-platform mobile development. By sharing business logic while keeping native UI, you get code reuse without sacrificing platform-specific user experience. Start with networking and data layers, expand to state management and business rules, and keep UI native. For teams already using Kotlin on Android, the learning curve is minimal — and iOS developers appreciate that KMP doesn’t replace their tools.
In conclusion, Kotlin Multiplatform Mobile is an essential topic for modern software development. By applying the patterns and practices covered in this guide, you can build more robust, scalable, and maintainable systems. Start with the fundamentals, iterate on your implementation, and continuously measure results to ensure you are getting the most value from these approaches.