Kotlin Multiplatform in 2026: Sharing Code Between Android and iOS

How Kotlin Multiplatform shares business logic across Android and iOS in 2026, with Gradle setup, expect/actual, Ktor networking, Compose Multiplatform UI, and common KMP interview questions.

Kotlin Multiplatform 2026 sharing code between Android and iOS apps

Kotlin Multiplatform (KMP) lets one Kotlin codebase power the business logic of both Android and iOS apps while each platform keeps its fully native UI, performance, and access to its SDK. In 2026 KMP is no longer an experiment: it is stable, officially backed by Google, and shipping in production at Netflix, McDonald's, Forbes, and Cash App. This deep dive covers how the code sharing actually works, how to configure a shared module, and the trade-offs worth understanding before a KMP interview.

What KMP actually shares

Kotlin Multiplatform shares compiled logic, not a runtime. Common Kotlin compiles to JVM bytecode for Android and to a native binary (via LLVM) for iOS. There is no JavaScript bridge, no embedded webview, and no reflection layer, so shared code runs at native speed on both platforms.

How Kotlin Multiplatform Shares Code Without Sacrificing Native Performance

The core mechanism is a hierarchy of source sets. commonMain holds platform-agnostic Kotlin: networking, serialization, business rules, and view models. Platform source sets like androidMain and iosMain hold code that touches platform APIs. The compiler produces a JVM artifact from commonMain plus androidMain for Android, and a native framework from commonMain plus iosMain for iOS.

Kotlin Multiplatform in 2026 favors sharing what genuinely benefits from a single source of truth: models, API clients, caching, and validation. UI can be shared with Compose Multiplatform or left fully native with SwiftUI, screen by screen. This "share the logic, choose the UI" model is why teams adopt KMP without rewriting existing apps.

Kotlin Multiplatform is a Kotlin technology that compiles one shared module to multiple targets (Android on the JVM, iOS as native, plus desktop and web) so business logic is written once while each platform retains its native UI and full SDK access. Google now supports it as a first-class option, with Jetpack libraries such as Room, DataStore, ViewModel, and Paging offering KMP artifacts.

Deciding what belongs in the shared module is the single most important architectural call in a KMP project. The table below reflects how most production teams draw the line in 2026: everything below the presentation layer is a strong candidate for sharing, while anything that touches platform UX conventions usually stays native.

| Layer | Share in commonMain? | Reason | |-------|---------------------|--------| | Data models and DTOs | Yes | Identical API contract on both platforms | | Networking and caching | Yes | One HTTP stack, one bug surface | | Business rules and validation | Yes | The domain does not differ per OS | | View models and state | Usually | Compose and SwiftUI both consume shared state | | UI rendering | Optional | Shared with Compose, or native per screen | | Platform APIs (camera, biometrics) | No | Expose through expect/actual instead |

The payoff scales with how much of the lower half of that table an app owns. Data-heavy products with thin UIs share the most; highly bespoke, animation-driven apps share the least. Neither extreme is wrong, which is why KMP suits incremental adoption rather than an all-or-nothing rewrite.

Configuring the Shared Module in Gradle

A KMP module declares its targets and source sets in Kotlin DSL. The following build.gradle.kts sets up Android and the three iOS architectures (physical device, Intel simulator, Apple Silicon simulator) and exposes the iOS output as a framework named Shared.

shared/build.gradle.ktskotlin
plugins {
    kotlin("multiplatform")
    kotlin("plugin.serialization")
    id("com.android.library")
}

kotlin {
    // Android target compiles commonMain + androidMain to JVM bytecode
    androidTarget()

    // Three iOS targets: device, Intel sim, Apple Silicon sim
    listOf(iosArm64(), iosX64(), iosSimulatorArm64()).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"   // becomes "import Shared" in Swift
            isStatic = true
        }
    }

    sourceSets {
        commonMain.dependencies {
            implementation("io.ktor:ktor-client-core:3.3.0")
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
        }
        androidMain.dependencies {
            implementation("io.ktor:ktor-client-okhttp:3.3.0")
        }
        iosMain.dependencies {
            implementation("io.ktor:ktor-client-darwin:3.3.0")
        }
    }
}

Each target pulls a platform-specific Ktor engine (okhttp on Android, darwin on iOS) while the shared code depends only on the common ktor-client-core API. The full option set lives in the Kotlin Multiplatform documentation.

Writing Platform-Specific Code With expect and actual

Some logic cannot live in commonMain because it needs a platform API. The expect/actual mechanism declares the shape in common code and supplies a concrete implementation per target, resolved at compile time with zero runtime cost.

commonMain/kotlin/Platform.ktkotlin
// commonMain declares what it needs but cannot implement here
expect class Platform() {
    val name: String
    val osVersion: String
}

// Shared logic can now depend on Platform freely
fun analyticsUserAgent(): String {
    val platform = Platform()
    return "SharpSkill/" + platform.name + " " + platform.osVersion
}

The Android implementation reads from the Build class, and the iOS implementation reads from UIDevice. Both satisfy the same contract, so commonMain never knows which one it is talking to.

androidMain/kotlin/Platform.ktkotlin
import android.os.Build

// actual supplies the Android-specific implementation
actual class Platform actual constructor() {
    actual val name: String = "Android"
    actual val osVersion: String = Build.VERSION.RELEASE
}
iosMain/kotlin/Platform.ktkotlin
import platform.UIKit.UIDevice

// actual uses UIKit directly, no Objective-C bridging code required
actual class Platform actual constructor() {
    actual val name: String = "iOS"
    actual val osVersion: String = UIDevice.currentDevice.systemVersion
}

Kotlin/Native ships typed bindings for the entire iOS SDK, so UIDevice, NSUserDefaults, and Foundation types are callable from Kotlin without hand-written glue.

Sharing Networking and Serialization Logic

Networking is the highest-value code to share because API contracts are identical on both platforms. A single Ktor client plus kotlinx.serialization replaces two parallel implementations. The suspend functions rely on Kotlin coroutines, which map cleanly to async/await on both sides.

commonMain/kotlin/data/InterviewRepository.ktkotlin
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable

// One serializable model, zero duplication across platforms
@Serializable
data class Question(val id: String, val prompt: String, val difficulty: String)

// One HttpClient, one JSON parser, one repository for both apps
class InterviewRepository {
    private val client = HttpClient {
        install(ContentNegotiation) { json() }
    }

    suspend fun fetchQuestions(tech: String): List<Question> =
        client.get("https://api.sharpskill.dev/questions/" + tech).body()
}

This repository, its retry logic, and its data models are written once. A bug fixed in the parser is fixed on Android and iOS simultaneously, which is where KMP pays back its setup cost. The same pattern extends to a shared database with SQLDelight, shared key-value storage, and shared dependency injection with Koin, so the entire data layer can live in commonMain behind interfaces the UI depends on.

A common mistake is trying to share too much too early. The pragmatic path is to start with one repository, prove the build and CI pipeline on both platforms, then move features in one at a time as confidence grows.

Ready to ace your Android interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Building Shared UI With Compose Multiplatform

Compose Multiplatform reached stable status on iOS in May 2025 (version 1.8.0) and reached 1.11.0 by mid-2026 with native text input and concurrent rendering on by default. A @Composable in commonMain renders on both platforms, and shared view models pair naturally with an MVVM or MVI architecture.

commonMain/kotlin/ui/QuestionList.ktkotlin
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

// This list renders identically on Android and iOS
@Composable
fun QuestionList(questions: List<Question>) {
    LazyColumn {
        items(questions) { question ->
            Card(modifier = Modifier.padding(8.dp)) {
                Text(text = question.prompt, modifier = Modifier.padding(16.dp))
            }
        }
    }
}

Sharing UI is optional and per screen. A team can share the entire interface with Compose Multiplatform, keep SwiftUI for the whole app and share only logic, or mix both. Screens that depend on platform-specific UX, such as widgets or App Clips, are usually left native.

Consuming Shared Code From Swift on iOS

The shared module compiles to a framework that Swift imports like any library. The friction historically was Kotlin suspend functions and Flow surfacing as awkward callback APIs. SKIE from Touchlab solves this by generating idiomatic Swift: suspend becomes native async/await and Flow becomes AsyncSequence.

iosApp/ContentView.swiftswift
import SwiftUI
import Shared   // the KMP framework, baseName = "Shared"

struct ContentView: View {
    @State private var questions: [Question] = []
    private let repository = InterviewRepository()

    var body: some View {
        List(questions, id: \.id) { question in
            Text(question.prompt)
        }
        .task {
            // With SKIE, the Kotlin suspend fun is a Swift async fun
            questions = (try? await repository.fetchQuestions(tech: "android")) ?? []
        }
    }
}

The iOS team writes ordinary Swift and SwiftUI. They consume shared models and repositories exactly as they would a native Swift package, which keeps KMP invisible to most of the iOS codebase.

Two rough edges are still worth naming in an interview. First, build times: Kotlin/Native compilation is slower than a pure Swift build, though the Gradle build cache and pre-built XCFramework distribution soften the daily cost. Second, debugging across the Swift-to-Kotlin boundary is improving but not yet as seamless as staying in one language, which is another reason teams keep the shared surface deliberate rather than sprawling.

Kotlin Multiplatform Interview Questions to Expect

Since KMP interview questions increasingly appear in Android and mobile roles, a few recurring ones are worth rehearsing alongside broader Android interview preparation:

  • How does expect/actual differ from an interface? expect/actual resolves at compile time per target with no runtime dispatch and can back a class, function, property, or typealias. An interface resolves dynamically at runtime. Use expect/actual for platform APIs and interfaces for polymorphism within shared code.
  • How does Kotlin/Native handle memory and threading? Since Kotlin 1.7.20 the new memory manager removed the old object-freezing model, so shared mutable state and coroutines work across threads much as they do on the JVM.
  • Can an existing app adopt KMP incrementally? Yes. The shared module ships as an AAR for Android and an XCFramework for iOS, so one repository or feature can migrate at a time without a rewrite.
  • When should UI stay native instead of using Compose Multiplatform? When a screen leans on platform-specific UX or when the iOS team owns and prefers to control the presentation layer.
Toolchain in 2026

Target Kotlin 2.3 with the K2 compiler, Ktor 3.x for networking, SQLDelight for a shared database, and Koin for dependency injection. For iOS interop, add SKIE early; retrofitting it after the Swift layer is written is far more painful.

Conclusion

Kotlin Multiplatform in 2026 is a pragmatic way to cut duplicated logic across Android and iOS without giving up native UI or performance:

  • Share the logic that has a single source of truth (models, networking, validation) and choose native or Compose UI per screen.
  • Use expect/actual for platform APIs; it resolves at compile time with no runtime overhead.
  • Write networking once with Ktor and kotlinx.serialization so fixes land on both platforms at once.
  • Adopt incrementally through an AAR and XCFramework rather than committing to a rewrite.
  • Add SKIE from the start so Swift consumes suspend and Flow as native async/await and AsyncSequence.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#Kotlin Multiplatform
#KMP
#Android
#iOS
#Cross-platform

Share

Related articles