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() or jvm() — 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.
  • expect and actual: 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 .aar or .jar file, consumed like any other Gradle dependency.

  • iOS:
    KMP outputs a .framework or .xcframework bundle 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:

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/actual for 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

  1. A realistic starting point is to share your networking layer.
  2. Create models and API clients in commonMain.
  3. Implement platform-specific HTTP engines (OkHttp for Android, Darwin for iOS).
  4. 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

  1. 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.

  2. 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
  3. 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.