Kotlin Multiplatform (KMP): One Codebase, Many Platforms
Introduction
Kotlin Multiplatform (KMP) is JetBrains’ solution for teams who want to share logic across platforms — built for teams who want to share logic without giving up native UI.
Unlike Flutter or React Native, KMP doesn’t replace your UI layer — it complements it. You still write SwiftUI, Jetpack Compose, or React components natively, ensuring platform-native performance and APIs.
It’s about sharing logic, not necessarily UI — although UI sharing is possible with frameworks like Compose Multiplatform.
Note: Compose Multiplatform extends KMP by also sharing UI code (using Jetpack Compose), but that’s optional — KMP works just fine without it.
KMP is Gradle-driven, compiles Kotlin to each platform’s native/bytecode target, and exposes Kotlin artifacts you can consume directly from platform code.
Why Use KMP
- Less duplicate logic: single implementation for algorithms, validation, networking.
- Consistency: same models, serializers, and business rules across platforms.
- Maintainability: fixes and features applied once propagate everywhere.
- Flexibility: keep platform-specific UI idiomatic (Compose on Android, SwiftUI on iOS).
- Incremental adoption: migrate small pieces at a time — no big-bang rewrite.
️ Core Concepts
Targets
KMP compiles shared code to platform-specific artifacts. Common targets include:
android()orjvm()— compiles to Java bytecode (Android builds into an AAR).iosX64,iosArm64,iosSimulatorArm64— iOS frameworks (Objective-C/Swift interop).js()— compiles to JavaScript (Node/browser).wasmJs()— compiles to WebAssembly for high-performance web applications.macosX64,linuxX64,mingwX64— native/desktop targets.
Source Sets
Source sets organize shared and platform-specific code.
commonMain/commonTest: shared code and tests.androidMain,iosMain,jvmMain: platform implementations.expectandactual: expect and actual are Kotlin’s mechanism for defining platform-agnostic APIs — more on this below
Example:
// commonMain
expect fun currentTimestampMillis(): Long
// androidMain
actual fun currentTimestampMillis(): Long = System.currentTimeMillis()
// iosMain
actual fun currentTimestampMillis(): Long = NSDate().timeIntervalSince1970.toLong() * 1000L
Expect / Actual Pattern
This pattern allows you to define APIs in shared code (expect) and implement them in platform-specific code (actual).
It’s the foundation of how Kotlin Multiplatform abstracts platform differences.
For example:
// commonMain
expect class Logger() {
fun log(message: String)
}
// androidMain
actual class Logger {
actual fun log(message: String) {
Log.d("KMP", message)
}
}
// iosMain
actual class Logger {
actual fun log(message: String) {
println("KMP: $message")
}
}
You’ll often use this for:
- Logging
- File I/O
- Network clients (Ktor engines)
- Preferences
- System services (camera, GPS, etc.)
Interoperability
Kotlin Multiplatform integrates smoothly with each target’s ecosystem.
-
Android / JVM:
The shared module compiles to a.aaror.jarfile, consumed like any other Gradle dependency. -
iOS:
KMP outputs a.frameworkor.xcframeworkbundle exposing Objective-C headers.
Swift can then call shared Kotlin code directly. -
JavaScript:
Generates JS bundles for Node or browser, allowing Kotlin logic to be reused in web apps.
Example — importing shared code in Swift:
import Shared
let userRepo = SharedUserRepository()
userRepo.getUser(id: "42") { user, error in
if let user = user {
print(user.name)
}
}
Build & Tooling
Gradle Setup
plugins {
kotlin("multiplatform") version "1.9.x" // Note: Replace .x with the latest stable version.
id("com.android.library")
}
kotlin {
android()
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.x")
implementation("io.ktor:ktor-client-core:2.x")
}
}
}
}
CocoaPods Integration
For iOS projects using CocoaPods, KMP supports automatic generation of .podspec files.
kotlin {
cocoapods {
summary = "Shared KMP Module"
homepage = "https://yourcompany.dev"
framework {
baseName = "shared"
}
}
}
After running pod install, your iOS app can import Shared like any other pod.
For Swift Package Manager: KMP also supports SPM via XCFrameworks — run ./gradlew :shared:assembleXCFramework and add it to Xcode manually or via Package.swift.
Multiplatform Libraries Ecosystem
Several Kotlin libraries already support KMP out of the box, including:
- Ktor — Networking
- kotlinx.serialization — JSON serialization
- SQLDelight — Type-safe database
- Koin or Kotlin Inject — Dependency injection
- kotlinx.coroutines — Concurrency
These libraries expose a commonMain implementation and internally manage platform-specific code.
Networking & Serialization
Networking in KMP is commonly implemented using Ktor, and data models are serialized using kotlinx.serialization.
Example setup:
// commonMain
expect fun provideHttpClient(): HttpClient
// androidMain
actual fun provideHttpClient(): HttpClient = HttpClient(OkHttp)
// iosMain
actual fun provideHttpClient(): HttpClient = HttpClient(Ios)
Persistence
You can implement persistence in a multiplatform-friendly way using:
- SQLDelight — for type-safe local database access
- Settings library or
expect/actualfor key-value storage
Example using expect/actual for preferences:
// commonMain
expect class Preferences() {
fun putString(key: String, value: String)
fun getString(key: String): String?
}
// androidMain
actual class Preferences {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
actual fun putString(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
actual fun getString(key: String): String? = prefs.getString(key, null)
}
// iosMain
actual class Preferences {
private val defaults = NSUserDefaults.standardUserDefaults()
actual fun putString(key: String, value: String) {
defaults.setObject(value, forKey = key)
}
actual fun getString(key: String): String? = defaults.stringForKey(key)
}
Migration Strategy (From Android-only Kotlin)
One of Kotlin Multiplatform’s biggest advantages is that you don’t need to rewrite your entire app.
You can adopt it gradually — starting with isolated modules and expanding over time.
Here’s a proven step-by-step migration plan:
1. Identify Reusable Logic
Start by locating modules that can easily be shared:
- Networking layer (API clients, DTOs)
- Models and serialization
- Business/domain logic
- Validation and utilities
Anything not tied to Android APIs (like Context or View) is a good candidate.
2. Create a Shared KMP Module
Create a new Gradle module (e.g., /shared) using the Kotlin Multiplatform plugin:
plugins {
kotlin("multiplatform")
id("com.android.library")
}
Configure targets for Android and iOS:
{
android()
iosX64()
iosArm64()
iosSimulatorArm64()
}
Add commonMain, androidMain, and iosMain source sets to start building shared functionality.
3. Move Common Code Incrementally
Move business logic, API definitions, and models from your Android module into commonMain. Replace Android dependencies (e.g., SharedPreferences) with expect/actual abstractions.
// commonMain
expect fun getAppVersion(): String
// androidMain
actual fun getAppVersion(): String =
BuildConfig.VERSION_NAME
4. Integrate Shared Code Back into Android
Once the shared module builds successfully, integrate it into your Android project.
In androidApp/build.gradle.kts:
implementation(project(":shared"))
Now your Android app uses logic from the shared module transparently.
5. Generate the iOS Framework
Next, build the iOS target to generate an .xcframework:
./gradlew :shared:assembleXCFramework
This produces a Shared.xcframework which can be imported into Xcode.
In Swift:
import Shared
let repo = SharedUserRepository()
repo.fetchUser(id: "42") { user in
print(user.name)
}
6. Gradually Expand
Once both Android and iOS consume shared logic, you can migrate more modules:
-
Caching and persistence
-
Analytics
-
Feature toggles
-
Utility layers
Avoid moving platform-specific code (UI, sensors, Bluetooth, etc.) until you have solid common infrastructure.
Best Practices During Migration
- Keep commonMain platform-agnostic — no Android imports.
- Write shared unit tests early in commonTest.
- Use dependency injection for platform services.
- Test build pipelines on macOS early to avoid CI surprises.
- Document all expect/actual pairs to prevent confusion later.
Example of a Small First Step
- A realistic starting point is to share your networking layer.
- Create models and API clients in
commonMain. - Implement platform-specific HTTP engines (OkHttp for Android, Darwin for iOS).
- Verify integration on Android first, then export to iOS.
This way, you deliver immediate value while reducing risk.
1.Testing Strategy KMP supports shared unit tests in commonTest:
// Testing Shared Code
// commonTest
class UserRepositoryTest {
@Test
fun testUserParsing() {
val user = User("John", 30)
assertEquals("John", user.name)
}
}
Run tests for all targets via:
./gradlew allTests
-
Performance & Binary Size Address a common concern: Does KMP add bloat? Kotlin/Native binaries are optimized with tree-shaking and minification. Shared code typically adds minimal overhead compared to duplicating logic in Swift and Kotlin separately.
- Building KMP in CI/CD macOS runners required for iOS builds (GitHub Actions, Bitrise, etc.) Use ./gradlew build for all targets or separate jobs per platform Cache Gradle and CocoaPods dependencies to speed up builds
- Limitations Being honest about trade-offs builds trust: Current Limitations: iOS debugging experience is improving but not as mature as Android Some Kotlin/JVM libraries don’t support Kotlin/Native yet Swift interop for advanced generics can be tricky
Conclusion
Kotlin Multiplatform is not just another cross-platform framework — it’s a multi-target architecture built on top of Kotlin’s language power and Gradle’s flexibility.
Instead of forcing a single UI or runtime across devices, KMP gives you the freedom to share only what makes sense — your business logic, models, and data layers — while keeping the native experience intact for every platform.
For Android developers, KMP feels natural. You can reuse your Kotlin skills, libraries, and build tools while extending your reach to iOS, Desktop, or even Web.
It enables consistency in logic, faster feature parity, and fewer bugs across platforms — without sacrificing performance or native design principles.
The key to success with KMP lies in incremental adoption. Start small — share stable, core modules like networking or validation — then expand gradually as your team and tooling mature.
As the ecosystem evolves with Compose Multiplatform, SQLDelight, Ktor, and Kotlin/Native improvements, Kotlin Multiplatform is fast becoming a production-ready solution for modern, scalable, and maintainable mobile architectures.
Build once. Run everywhere. Stay native.
