Kotlin Multiplatform Mobile: Cross-Platform Code Sharing
The cross-platform mobile development conversation typically centres on React Native and Flutter: frameworks that share both UI and business logic across platforms. Kotlin Multiplatform Mobile (KMM) takes a fundamentally different approach. It shares only the business logic while keeping the UI entirely native.
This distinction matters. Native UIs feel native because they are native. KMM lets you write your networking, data models, business rules, and storage once in Kotlin, while building your iOS UI with SwiftUI or UIKit and your Android UI with Jetpack Compose or XML layouts.
KMM is currently in alpha (as of November 2021), backed by JetBrains and adopted by companies including Netflix, VMware, and Cash App. This guide explores what KMM is, when it makes sense, and how to get started.
How KMM Works
KMM builds on Kotlin/Native, which compiles Kotlin code to native binaries for multiple platforms. The architecture has three layers:
[iOS Native UI] [Android Native UI]
(SwiftUI/UIKit) (Compose/XML)
| |
[iOS-specific code] [Android-specific code]
(Swift/Kotlin) (Kotlin)
| |
+-------+-------+
|
[Shared Kotlin Code]
(Business logic, networking,
data models, storage)
The shared module compiles to:
- A Kotlin library for Android (standard Kotlin/JVM)
- A native framework for iOS (via Kotlin/Native)
iOS developers consume the shared code as a regular framework, calling Kotlin code from Swift as if it were Objective-C.
When KMM Makes Sense
Ideal Scenarios
Separate iOS and Android teams. If you have dedicated native developers for each platform, KMM lets them keep their preferred tools and languages while sharing the business logic that would otherwise be written twice.
Complex business logic. Apps with significant domain logic (financial calculations, data processing, complex validation rules) benefit most from sharing that logic once.
Existing native apps. KMM can be adopted incrementally. You do not need to rewrite your app. Start by sharing a single module.
Performance-critical apps. Since the UI remains native, there is no performance penalty for rendering. The shared code compiles to native binaries.
Less Ideal Scenarios
Small, simple apps. The overhead of setting up KMM may not be justified if your app’s business logic is trivial.
Solo developers. If one developer maintains both platforms, a full cross-platform framework (Flutter, React Native) may be more efficient.
UI-heavy apps with minimal logic. If your app is mostly UI (gallery, media player), there is little business logic to share.
Setting Up a KMM Project
Prerequisites
- Android Studio Arctic Fox (2020.3.1) or later
- KMM plugin for Android Studio
- Xcode 12 or later (for iOS compilation)
Install the KMM plugin: Android Studio, then Preferences, then Plugins, search for “Kotlin Multiplatform Mobile.”
Project Structure
Create a new KMM project from the template. The resulting structure:
my-app/
androidApp/ # Android application module
src/main/
java/
MainActivity.kt
iosApp/ # iOS application (Xcode project)
iosApp/
ContentView.swift
shared/ # Shared KMM module
src/
commonMain/ # Shared code (all platforms)
kotlin/
Platform.kt
Greeting.kt
androidMain/ # Android-specific implementations
kotlin/
Platform.kt
iosMain/ # iOS-specific implementations
kotlin/
Platform.kt
build.gradle.kts
The commonMain source set contains platform-independent code. androidMain and iosMain contain platform-specific implementations.
Shared Module Configuration
// shared/build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
kotlin("plugin.serialization")
}
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0")
implementation("io.ktor:ktor-client-core:1.6.4")
implementation("io.ktor:ktor-client-serialization:1.6.4")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:1.6.4")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:1.6.4")
}
}
}
}
Sharing Business Logic
Data Models
Define data models once in commonMain:
// shared/src/commonMain/kotlin/models/Task.kt
@Serializable
data class Task(
val id: String,
val title: String,
val description: String,
val isCompleted: Boolean,
val priority: Priority,
val dueDate: String? = null,
val createdAt: String
)
@Serializable
enum class Priority {
LOW, MEDIUM, HIGH, URGENT
}
These models are available on both platforms. Android uses them as regular Kotlin classes. iOS uses them as Objective-C-compatible classes (accessible from Swift).
Networking
Use Ktor (a Kotlin HTTP client) for shared networking:
// shared/src/commonMain/kotlin/api/TaskApi.kt
class TaskApi(private val baseUrl: String) {
private val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
suspend fun getTasks(): List<Task> {
return client.get("$baseUrl/api/tasks")
}
suspend fun createTask(request: CreateTaskRequest): Task {
return client.post("$baseUrl/api/tasks") {
contentType(ContentType.Application.Json)
body = request
}
}
suspend fun updateTask(id: String, request: UpdateTaskRequest): Task {
return client.put("$baseUrl/api/tasks/$id") {
contentType(ContentType.Application.Json)
body = request
}
}
suspend fun deleteTask(id: String) {
client.delete<Unit>("$baseUrl/api/tasks/$id")
}
}
Business Logic
Share validation, calculations, and business rules:
// shared/src/commonMain/kotlin/domain/TaskValidator.kt
class TaskValidator {
fun validateTitle(title: String): ValidationResult {
return when {
title.isBlank() -> ValidationResult.Error("Title cannot be empty")
title.length > 200 -> ValidationResult.Error("Title must be under 200 characters")
else -> ValidationResult.Valid
}
}
fun canDeleteTask(task: Task, currentUserId: String): Boolean {
return task.createdBy == currentUserId || task.isCompleted
}
}
sealed class ValidationResult {
object Valid : ValidationResult()
data class Error(val message: String) : ValidationResult()
}
Platform-Specific Code with expect/actual
When you need platform-specific implementations, use the expect/actual mechanism:
// commonMain: Declare expectation
expect class PlatformStorage {
fun getString(key: String): String?
fun putString(key: String, value: String)
}
// androidMain: Android implementation
actual class PlatformStorage(private val context: Context) {
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
actual fun getString(key: String): String? = prefs.getString(key, null)
actual fun putString(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
}
// iosMain: iOS implementation
actual class PlatformStorage {
private val defaults = NSUserDefaults.standardUserDefaults
actual fun getString(key: String): String? = defaults.stringForKey(key)
actual fun putString(key: String, value: String) {
defaults.setObject(value, forKey = key)
}
}
Consuming Shared Code
From Android
Shared code is consumed as a regular Kotlin dependency:
// androidApp: ViewModel
class TaskViewModel(
private val taskApi: TaskApi,
private val validator: TaskValidator
) : ViewModel() {
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
fun loadTasks() {
viewModelScope.launch {
_tasks.value = taskApi.getTasks()
}
}
fun addTask(title: String) {
val validation = validator.validateTitle(title)
if (validation is ValidationResult.Valid) {
viewModelScope.launch {
taskApi.createTask(CreateTaskRequest(title = title))
loadTasks()
}
}
}
}
From iOS (Swift)
The shared module is distributed as an iOS framework:
import shared // The KMM framework
class TaskViewModel: ObservableObject {
private let taskApi = TaskApi(baseUrl: "https://api.yourapp.com")
private let validator = TaskValidator()
@Published var tasks: [Task] = []
func loadTasks() {
taskApi.getTasks { [weak self] tasks, error in
if let tasks = tasks {
DispatchQueue.main.async {
self?.tasks = tasks
}
}
}
}
func addTask(title: String) -> String? {
let validation = validator.validateTitle(title: title)
if validation is ValidationResult.Error {
return (validation as! ValidationResult.Error).message
}
taskApi.createTask(
request: CreateTaskRequest(title: title)
) { [weak self] _, _ in
self?.loadTasks()
}
return nil
}
}
Note: Kotlin coroutines map to callback-based APIs on iOS. Libraries like KMP-NativeCoroutines can improve this experience by providing Swift async/await interop.
Current Limitations
KMM is alpha software. Be aware of these limitations:
Kotlin/Native memory model. The current memory model has restrictions on sharing mutable state across threads. A new memory model is in development that removes these restrictions.
Swift interop. Kotlin/Native compiles to Objective-C-compatible binaries, not Swift. This means some Kotlin features (sealed classes, enums with data) map imperfectly to Swift. Generics support is limited.
Debugging. Debugging shared code from Xcode is possible but not seamless. Android Studio provides a better debugging experience for the shared module.
Build times. iOS builds can be slow because Kotlin/Native compilation is still being optimised.
Ecosystem maturity. Fewer multiplatform libraries exist compared to Flutter or React Native ecosystems. The core libraries (Ktor, Serialization, Coroutines) are solid, but niche functionality may require platform-specific implementations.
Who Is Using KMM
- Netflix: Uses KMM for their Prodicle app (production management)
- VMware: Shares business logic across mobile platforms
- Cash App: Adopted KMM for shared domain logic
- Philips: Uses KMM in healthcare applications
These adoptions by major companies signal growing confidence in the technology.
Should You Adopt KMM Today?
For production apps (November 2021): Proceed with caution. KMM is alpha, and breaking changes are possible. Adopt it for non-critical shared modules (data models, validation, analytics) rather than core functionality.
For new projects starting in 2022: KMM is worth serious consideration, especially if you plan to maintain separate native UIs. The trajectory is positive, and the stable release is expected in 2022.
For teams evaluating cross-platform approaches: If native UI quality is non-negotiable but code duplication bothers you, KMM offers a unique middle ground that React Native and Flutter cannot match.
At eawesome, we are experimenting with KMM for shared business logic in projects where our clients maintain separate iOS and Android teams. The promise of writing business rules once while keeping native UIs is compelling, and we are cautiously optimistic about KMM’s future.