Kotlin Multiplatform

Production engineering guide for kotlin multiplatform covering patterns, implementation strategies, and operational best practices.

Kotlin Multiplatform (KMP) enables shared business logic across iOS, Android, and other platforms using a single codebase written in Kotlin. It matters when teams need to maintain consistent behavior across mobile platforms, reduce duplicated effort, and deliver features faster with a unified backend and frontend logic. KMP is especially valuable when shared logic includes complex state management, domain models, network layers, and offline capabilities.

Defining and Configuring a KMP Project

A KMP project starts with a Gradle build script declaring shared and platform-specific modules. The core configuration resides in build.gradle.kts (or build.gradle for Groovy), where kotlin and kotlin-multiplatform plugins are applied.

plugins {
    id("org.jetbrains.kotlin.multiplatform") version "1.9.20"
    id("org.jetbrains.kotlin.android") version "1.9.20"
    id("org.jetbrains.kotlinx.kover") version "0.7.3"
}

kotlin {
    jvm()
    android()
    ios()
    js {
        browser()
        // Additional JS-specific configuration
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val androidMain by getting
        val iosMain by getting
        val jvmMain by getting
        val jsMain by getting
    }
}

The sourceSets block defines which code runs on which platforms. The commonMain source set contains logic shared across all targets. Platform-specific code lives in platformMain sets like androidMain, iosMain, etc. The kotlin-multiplatform plugin automatically generates platform-specific artifacts and manages interop.

Platform-Specific Configuration and Dependencies

Each platform requires specific setup in the Gradle configuration.

For Android, the android() block enables Android-specific features like compileSdk, minSdk, and targetSdk:

android {
    compileSdk = 34
    namespace = "com.example.shared"
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
}

For iOS, the ios() block sets up the target architecture and enables Swift interop:

ios {
    // Default is "iosArm64" for Apple Silicon
    binaries {
        framework {
            baseName = "SharedLogic"
            // Export to Swift as a framework
        }
    }
    // Specify the target device family
    iosTarget {
        deviceFamily = "iphone"
        simulator = true
    }
}

The framework binary generates a .framework directory that can be embedded in Xcode projects or consumed by Swift code. The baseName determines the name of the framework file, which is used in import SharedLogic statements.

Building and Packaging Artifacts

Build artifacts are produced with the build task, which compiles all platform-specific versions of the shared code.

./gradlew build

This generates:

For Android, the .aar file includes .jar with Kotlin metadata, res/, assets/, and AndroidManifest.xml. For iOS, the .framework contains Headers/, Modules/, and libSharedLogic.a.

To publish to a Maven repository, use the maven-publish plugin:

publishing {
    repositories {
        maven {
            url = uri("https://repo.example.com/releases")
            credentials {
                username = "admin"
                password = "secret"
            }
        }
    }
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
            // Include Kotlin metadata
            artifactId = "shared-logic"
            version = "1.0.0"
            pom {
                name.set("Kotlin Multiplatform Shared Logic")
                description.set("Shared business logic across Android and iOS")
            }
        }
    }
}

Using Shared Code in Platform-Specific Projects

Android: Consuming KMP Libraries

In an Android app, the shared library is imported via Gradle:

dependencies {
    implementation("com.example:shared-logic:1.0.0")
}

The shared code can be used in Kotlin, Java, and even in XML layouts via @Binding and @BindingAdapter.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val calculator = Calculator()
        val result = calculator.add(3, 5)
        findViewById<TextView>(R.id.result).text = result.toString()
    }
}

The Calculator class, defined in commonMain, is available across both Kotlin and Java code. Kotlin’s @JvmOverloads and @JvmStatic annotations are respected during interop.

iOS: Integrating KMP with Swift

In an Xcode project, the SharedLogic.framework is added via File > Add Files to Project, and the import SharedLogic statement is used in Swift:

import UIKit
import SharedLogic

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let calculator = Calculator()
        let result = calculator.add(a: 3, b: 5)
        let label = UILabel()
        label.text = "\(result)"
        view.addSubview(label)
    }
}

The Calculator class is exposed to Swift through Kotlin’s metadata, which includes type information, function signatures, and extension points. The add(a:b:) function is automatically mapped to Swift’s named parameters.

JavaScript: Embedding in Web Apps

In a web application, the shared code is bundled using webpack or rollup.

{
  "name": "my-web-app",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "shared-logic": "1.0.0"
  },
  "devDependencies": {
    "webpack": "^5.88.0"
  }
}

A webpack.config.js file:

const path = require('path');

module.exports = {
    entry: './src/index.ts',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    resolve: {
        extensions: ['.js', '.ts', '.kotlin']
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: 'ts-loader'
            }
        ]
    }
};

The shared-logic.js is imported and used as a CommonJS module:

const { Calculator } = require('shared-logic');

const calc = new Calculator();
const result = calc.add(2, 3);
console.log(result);

Interoperability and Data Sharing

Sharing Data Models

Data models are shared via expect/actual declarations or direct Kotlin classes.

// commonMain/kotlin/model/Person.kt
expect class Person {
    val name: String
    val age: Int
    fun introduce(): String
}

// androidMain/kotlin/model/Person.kt
actual class Person actual constructor(
    override val name: String,
    override val age: Int
) : Person() {
    override fun introduce(): String = "Hi, I'm $name and I'm $age years old."
}

The Person class is defined once in commonMain, and platform-specific implementations are provided in androidMain and iosMain. The actual keyword links the expect declaration to its implementation.

Serializing and Deserializing Data

KMP uses kotlinx.serialization for JSON and binary data.

@Serializable
data class User(
    val id: Int,
    val name: String,
    val email: String
)

Serialization is used across platforms:

val user = User(1, "Alice", "[email protected]")
val json = Json {
    encodeDefaults = true
    ignoreUnknownKeys = true
    explicitNulls = true
}
val jsonString = json.encodeToString(User.serializer(), user)

On iOS, the same User class is serialized to JSON using JSONSerialization:

let user = User(id: 1, name: "Alice", email: "[email protected]")
let jsonData = try JSONEncoder().encode(user)
let jsonString = String(data: jsonData, encoding: .utf8)!

Handling Platform-Specific APIs

Platform-specific logic is implemented in platformMain source sets:

// iosMain/kotlin/platform/PlatformUtils.kt
actual fun getDeviceName(): String = 
    UIDevice.current.model

This function is called from commonMain code, with the actual implementation provided only in iOS.

Common Failures and Sharp Edges

Missing Kotlin Metadata

When a KMP library is used in a non-Kotlin project (e.g., Swift or Java), missing Kotlin metadata causes ClassNotFoundException or KotlinNullPointerException at runtime, even if all classes are present.

The metadata is generated by the kotlin-compiler and must be included in the JAR or framework. For Android, the kotlin-stdlib module must be bundled or declared in build.gradle.

Duplicate Definitions and Conflicting Dependencies

If multiple KMP libraries depend on kotlinx-coroutines-core, and each includes its own version, the runtime may load multiple instances of CoroutineContext, leading to CoroutineContext mismatches and ClassCastException.

The kotlinx.coroutines library must be published with kotlinx-coroutines-core as a transitive dependency, and platform-specific modules must declare api dependencies.

Unexpected Type Mismatches in Interop

Swift code may pass a String to a Kotlin function expecting kotlin.String, but the types differ at runtime. The kotlin.String is a class with metadata, while Swift’s String is a struct.

To fix this, use @JvmOverloads and @JvmStatic annotations, and ensure that the @Serializable annotations are applied consistently.

Failure in iOS Framework Packaging

When packaging the iOS framework, the Headers/ directory must contain all public types. If the SharedLogic.h file is missing or incorrectly generated, Swift code fails to compile with “No such module ‘SharedLogic’”.

The framework binary must be built with linker and kotlin flags to include metadata, and the module.modulemap must be correctly generated.

Debugging Kotlin Native Runtime Errors

Kotlin Native (K/N) logs are not automatically enabled. To enable logging, set the KOTLIN_NATIVE_LOGGING environment variable:

export KOTLIN_NATIVE_LOGGING=1

This enables detailed logs about memory management, threading, and interop.

In production, a kotlin-native crash can manifest as a SIGSEGV or KotlinNullPointerException without a stack trace. The kotlin-native runtime must be included in the app bundle, and the libkotlin and libkotlinx.coroutines libraries must be linked.

Summary

Kotlin Multiplatform enables true sharing of business logic across mobile and other platforms. Its strength lies in seamless interoperability between Kotlin, Java, Swift, and JavaScript. The key is proper Gradle configuration, correct handling of platform-specific code, and attention to metadata and packaging. Common pitfalls include missing Kotlin metadata, type mismatches in interop, and inconsistent serialization. With careful configuration and build practices, KMP delivers a consistent, performant, and maintainable shared codebase across platforms.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.