Kotlin 2.2: Multiplatform Development
HOW TO GUIDES Jan. 21, 2026, 11:30 a.m.

Kotlin 2.2: Multiplatform Development

Kotlin 2.2 has finally turned the long‑awaited “write once, run everywhere” dream into a practical reality for many teams. With its refined compiler, tighter Gradle integration, and first‑class support for iOS, Web, and desktop, Kotlin Multiplatform (KMP) is no longer a niche experiment—it’s a production‑ready toolkit. In this article we’ll walk through the core concepts, set up a real‑world shared module, and explore two practical examples that you can copy‑paste into your own projects.

Why Kotlin Multiplatform Matters in 2024

Multiplatform development isn’t new, but Kotlin’s approach stands out because it lets you share **business logic** while still writing idiomatic code for each UI layer. This means you avoid duplicated networking, validation, and domain models, reducing bugs and speeding up feature delivery.

Companies like Square, Netflix, and Lyft already ship shared Kotlin code to Android, iOS, and even JavaScript front‑ends. The payoff is measurable: up to 40 % fewer lines of platform‑specific code and a unified test suite that runs on the JVM, iOS simulators, and browsers.

Getting Started with Kotlin 2.2

Kotlin 2.2 ships with the new kmp Gradle plugin, which simplifies module configuration. The plugin automatically creates source sets for commonMain, androidMain, iosMain, and jsMain. All you need is a recent version of Android Studio (Arctic Fox or newer) and the Kotlin plugin set to 2.2.x.

plugins {
    kotlin("multiplatform") version "2.2.0"
    id("com.android.library")
}

Next, declare the targets you want to support. Kotlin 2.2 introduces the ios() shortcut, which creates both simulator and device binaries in one line.

kotlin {
    android()
    ios()               // creates iosX64, iosArm64, iosSimulatorArm64
    js(IR) {
        browser()
    }
    sourceSets {
        val commonMain by getting
        val androidMain by getting
        val iosMain by getting
        val jsMain by getting
    }
}

Structuring a Shared Module

A well‑structured shared module follows the classic “clean architecture” layers: domain, data, and presentation. Each layer lives in commonMain, while platform‑specific implementations (e.g., SQLite on Android, CoreData on iOS) sit in their respective source sets.

Here’s a quick folder layout:

  • src/commonMain/kotlin/com/example/shared/domain
  • src/commonMain/kotlin/com/example/shared/data
  • src/commonMain/kotlin/com/example/shared/presentation
  • src/androidMain/kotlin/com/example/shared/data/android
  • src/iosMain/kotlin/com/example/shared/data/ios

Keeping the platform‑agnostic code pure Kotlin (no Android or iOS imports) ensures you can run the same unit tests on the JVM.

Example 1: A Simple Calculator Library

Let’s build a tiny arithmetic library that can be used from an Android app, an iOS SwiftUI view, and a React‑like Kotlin/JS UI. The core logic lives in commonMain and is deliberately pure.

Domain Model

package com.example.shared.domain

sealed class Operation {
    data class Add(val a: Double, val b: Double) : Operation()
    data class Subtract(val a: Double, val b: Double) : Operation()
    data class Multiply(val a: Double, val b: Double) : Operation()
    data class Divide(val a: Double, val b: Double) : Operation()
}

Calculator Service

package com.example.shared.domain

class Calculator {
    fun compute(op: Operation): Double = when (op) {
        is Operation.Add -> op.a + op.b
        is Operation.Subtract -> op.a - op.b
        is Operation.Multiply -> op.a * op.b
        is Operation.Divide -> {
            require(op.b != 0.0) { "Division by zero" }
            op.a / op.b
        }
    }
}

Because this class uses only Kotlin stdlib, it compiles to JVM bytecode, native binaries, and JavaScript without any changes.

Android Usage

class MainActivity : AppCompatActivity() {
    private val calculator = Calculator()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val result = calculator.compute(Operation.Add(3.0, 5.0))
        findViewById<TextView>(R.id.result).text = "Result: $result"
    }
}

iOS Bridging

Expose the Kotlin class to Swift by adding an expect/actual wrapper (optional for pure Kotlin). The Gradle plugin automatically generates a framework you can import.

import SharedCalculator // generated framework name

let calc = Calculator()
let result = calc.compute(operation: .add(a: 3, b: 5))
print("Result: \\(result)")

JavaScript Integration

import { Calculator, Operation } from 'shared-calculator'

const calc = new Calculator()
const result = calc.compute(new Operation.Add(3, 5))
document.getElementById('output').textContent = `Result: ${result}`
Pro tip: When you need to expose Kotlin sealed classes to Swift/JS, generate companion factory functions in commonMain. They make interop painless and keep your UI code tidy.

Example 2: Cross‑Platform Networking with Ktor

Most real‑world apps need HTTP, so let’s create a small API client that works everywhere. Kotlin 2.2 ships with Ktor 2.3, which already supports JVM, Native, and JS transports.

API Contract (commonMain)

package com.example.shared.data

import io.ktor.client.*
import io.ktor.client.request.*

data class Post(val id: Int, val title: String, val body: String)

interface PostRepository {
    suspend fun fetchPosts(): List<Post>
}

Implementation Using Ktor (commonMain)

package com.example.shared.data

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*

class KtorPostRepository(
    private val client: HttpClient = HttpClient()
) : PostRepository {
    override suspend fun fetchPosts(): List<Post> = client.get("https://jsonplaceholder.typicode.com/posts").body()
}

The same class works on Android, iOS, and the browser because Ktor abstracts the underlying engine. You only need to add platform‑specific dependencies if you want to tune the engine (e.g., OkHttp on Android, Darwin on iOS).

Platform‑Specific Engine Configuration

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-core:2.3.0")
                implementation("io.ktor:ktor-client-content-negotiation:2.3.0")
                implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-okhttp:2.3.0")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-darwin:2.3.0")
            }
        }
        val jsMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-js:2.3.0")
            }
        }
    }
}

Consuming the Repository on Android

class PostsViewModel : ViewModel() {
    private val repo = KtorPostRepository()

    private val _posts = MutableLiveData<List<Post>>()
    val posts: LiveData<List<Post>> = _posts

    init {
        viewModelScope.launch {
            _posts.value = repo.fetchPosts()
        }
    }
}

iOS SwiftUI Example

import SwiftUI
import SharedData // generated framework

struct ContentView: View {
    @State private var posts: [Post] = []

    var body: some View {
        List(posts, id: \.id) { post in
            Text(post.title)
        }
        .onAppear {
            Task {
                let repo = KtorPostRepository()
                posts = try await repo.fetchPosts()
            }
        }
    }
}

Web Usage with Kotlin/JS

import kotlinx.browser.document
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch

val repo = KtorPostRepository()
val scope = MainScope()

scope.launch {
    val posts = repo.fetchPosts()
    val list = document.createElement("ul")
    posts.forEach {
        val li = document.createElement("li")
        li.textContent = it.title
        list.appendChild(li)
    }
    document.body?.appendChild(list)
}
Note: When targeting iOS, remember to enable the useIR flag in your Xcode project to avoid runtime crashes caused by mismatched Kotlin/Native binaries.

Platform‑Specific Implementations

Even with a shared core, you’ll inevitably need platform‑specific code—for example, accessing the device’s secure storage or using Android’s Jetpack DataStore. Kotlin’s expect/actual mechanism lets you declare an API in commonMain and provide concrete implementations where they belong.

Secure Token Storage

package com.example.shared.data

expect class SecureStorage {
    fun save(key: String, value: String)
    fun read(key: String): String?
}

Android implementation:

package com.example.shared.data.android

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import com.example.shared.data.SecureStorage

actual class SecureStorage(private val context: Context) {
    private val prefs = EncryptedSharedPreferences.create(
        "secure_prefs",
        MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
        context,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    actual fun save(key: String, value: String) {
        prefs.edit().putString(key, value).apply()
    }

    actual fun read(key: String): String? = prefs.getString(key, null)
}

iOS implementation (Native):

package com.example.shared.data.ios

import platform.Foundation.NSUserDefaults
import com.example.shared.data.SecureStorage

actual class SecureStorage {
    private val defaults = NSUserDefaults.standardUserDefaults

    actual fun save(key: String, value: String) {
        defaults.setObject(value, forKey = key)
    }

    actual fun read(key: String): String? = defaults.stringForKey(key)
}

Testing and Debugging Across Targets

Kotlin 2.2 makes it easier to run the same test suite on every platform. Place your tests in commonTest for pure logic, and add platform‑specific tests in androidTest, iosTest, or jsTest when you need to verify integration points.

package com.example.shared.domain

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class CalculatorTest {
    private val calc = Calculator()

    @Test fun `addition works`() {
        assertEquals(8.0, calc.compute(Operation.Add(3.0, 5.0)))
    }

    @Test fun `division by zero throws`() {
        assertFailsWith<IllegalArgumentException> {
            calc.compute(Operation.Divide(10.0, 0.0))
        }
    }
}

Run the suite with a single Gradle command:

./gradlew allTests

For iOS, the plugin automatically generates an Xcode scheme that runs the Kotlin tests on the simulator, letting you view results alongside native XCTest reports.

Build Optimizations and Incremental Compilation

Kotlin 2.2 introduces a new incremental compilation pipeline for native targets. The compiler now caches intermediate IR files, cutting down rebuild times from minutes to seconds on typical MacBook hardware.

  • Enable caching: kotlin.native.cacheKind = "experimental" in gradle.properties.
  • Use the kotlinx-serialization plugin only where needed to avoid unnecessary code generation.
  • Leverage cinterop stubs sparingly; each native library adds to the linking step.

When you combine these tricks with Gradle’s configuration cache, the overall CI pipeline can drop from 12 minutes to under 5 minutes for a full multiplatform build.

Pro Tips for Scaling KMP Projects

1️⃣ Keep the shared module thin. A 200 KB shared library is easier to reason about than a 5 MB monolith. Split large domains into separate Gradle sub‑projects if they evolve independently.

2️⃣ Prefer Flow over callbacks. Kotlin’s kotlinx.coroutines.flow works natively on all targets and gives you a unified reactive API.

3️⃣ Guard against API drift. Use the kmp-compatibility Gradle task (added in 2.2) to compare public signatures across targets and catch accidental platform‑only changes early.

4️⃣ Automate iOS framework packaging. Add a copyFramework task that produces an .xcframework, then reference it directly in Xcode’s Framework Search Paths.

5️⃣ Monitor binary size. Run ./gradlew :shared:linkReleaseFrameworkIosX64 and inspect the generated .klib. The klib‑size‑report plugin highlights which dependencies contribute most to the final size.

Future Roadmap: What’s Coming After 2.2?

Kotlin 2.3, slated for late 2026, promises first‑class support for SwiftUI previews, a unified compose runtime for web and native, and improved coroutine debugging on Native. The community also expects a “KMP Gradle DSL” that will replace the current kotlin { … } block with a more declarative syntax, making multi‑target configuration even more approachable.

Meanwhile, the Kotlin Multiplatform Mobile (KMM) SDK is being refactored to expose a single kmm artifact that bundles the most common libraries (Ktor, SQLDelight, and Coroutines). This will further lower the entry barrier for small teams that want to share code without wrestling with dependency versions.

Conclusion

Kotlin 2.

Share this article