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.
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.
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.
Build artifacts are produced with the build task, which compiles all platform-specific versions of the shared code.
./gradlew build
This generates:
build/libs/SharedLogic.framework for iOSbuild/libs/sharedlogic.jar for JVMbuild/libs/sharedlogic-android.aar for Androidbuild/libs/sharedlogic.js for JavaScriptFor 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")
}
}
}
}
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.
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.
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);
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.
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)!
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.
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.
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.
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.
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.
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.
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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy