Introduction
Kotlin Multiplatform (KMP) has matured into one of the most compelling solutions for cross-platform development in 2026. Unlike frameworks that generate entire applications from a single codebase, Kotlin Multiplatform focuses on sharing business logic while allowing developers to build native user interfaces for each platform.
This strategic approach addresses one of the fundamental challenges in cross-platform development: the trade-off between code reuse and platform-specific user experience. By sharing only the non-UI code, developers can achieve significant code reuse without compromising the native look and feel that users expect.
In 2026, Kotlin Multiplatform has achieved production-ready status with stable APIs, excellent tooling support, and adoption by major companies including Netflix, Airbnb, and VMware. The technology has evolved beyond mobile to support web and desktop applications, making it a comprehensive solution for organizations seeking to maximize code sharing across their product portfolio.
This comprehensive guide covers everything you need to know about Kotlin Multiplatform. From fundamental concepts to advanced architectural patterns, from setting up your first project to deploying to production, you’ll gain the knowledge needed to effectively leverage KMP in your development workflow.
Understanding Kotlin Multiplatform
The KMP Philosophy
Kotlin Multiplatform operates on a simple yet powerful principle: write business logic once in Kotlin, compile it for multiple platforms, and build native UIs for each platform using platform-specific technologies.
This approach differs fundamentally from other cross-platform solutions. Instead of generating UI code or using a common UI toolkit, KMP focuses exclusively on sharing the code that powers application logic. This includes:
- Data models and business rules
- Network and database operations
- Authentication and security logic
- Analytics and tracking code
- Validation and formatting utilities
The code that remains platform-specific typically includes:
- User interface implementation
- Platform-specific APIs (camera, GPS, notifications)
- Native navigation patterns
- Platform integration and permissions
How KMP Works
Kotlin Multiplatform uses Kotlin’s powerful type system and compilation model to generate platform-specific code. The process involves three main components:
The Kotlin compiler targets different platforms through platform-specific modules. For Kotlin/JVM, code compiles to Java bytecode. For Kotlin/JS, it compiles to JavaScript. For Kotlin/Native, it compiles to native binaries using LLVM.
// Shared code - works on all platforms
expect class Platform() {
val name: String
}
actual class Platform() {
val name: String = "Kotlin Multiplatform"
}
fun greet(): String {
return "Hello, ${Platform().name}!"
}
The expect and actual mechanism allows you to define platform-specific implementations. The shared module contains the expect declarations, while each platform module provides actual implementations.
Setting Up Your Project
Project Structure
A typical Kotlin Multiplatform project follows a specific directory structure:
my-app/
โโโ build.gradle.kts
โโโ settings.gradle.kts
โโโ gradle.properties
โโโ src/
โ โโโ commonMain/
โ โ โโโ kotlin/
โ โ โโโ com/example/app/
โ โ โโโ data/
โ โ โโโ domain/
โ โ โโโ Platform.kt
โ โโโ androidMain/
โ โ โโโ kotlin/
โ โ โโโ com/example/app/
โ โโโ iosMain/
โ โ โโโ kotlin/
โ โ โโโ com/example/app/
โ โโโ jsMain/
โ โ โโโ kotlin/
โ โ โโโ com/example/app/
โ โโโ desktopMain/
โ โโโ kotlin/
โ โโโ com/example/app/
Gradle Configuration
Modern KMP projects use Kotlin DSL for Gradle configuration:
// build.gradle.kts
plugins {
kotlin("multiplatform") version "2.0.21"
kotlin("android") version "2.0.21"
id("com.android.application")
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
iosX64Target()
iosArm64Target()
iosSimulatorArm64Target()
jsTarget(IR) {
browser()
nodejs()
}
linuxX64Target()
macosX64Target()
windowsX64Target()
sourceSets {
val commonMain by getting
val androidMain by getting
val iosMain by getting
val jsMain by getting
}
}
Dependencies Management
Dependencies in KMP projects are defined per source set:
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("io.ktor:ktor-client-core:2.3.8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
}
val androidMain by getting {
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-ios:2.3.8")
}
}
}
}
Sharing Business Logic
Data Models
One of the primary use cases for KMP is sharing data models across platforms. These models define the core data structures used throughout your application:
// commonMain/kotlin/com/example/app/model/User.kt
@Serializable
data class User(
val id: String,
val email: String,
val name: String,
val avatarUrl: String? = null,
val createdAt: Long,
val isPremium: Boolean = false
)
@Serializable
data class UserProfile(
val user: User,
val stats: UserStats,
val settings: UserSettings
)
@Serializable
data class UserStats(
val postsCount: Int,
val followersCount: Int,
val followingCount: Int
)
@Serializable
data class UserSettings(
val notificationsEnabled: Boolean = true,
val theme: Theme = Theme.SYSTEM,
val language: String = "en"
)
enum class Theme {
LIGHT, DARK, SYSTEM
}
Repository Pattern
The repository pattern works exceptionally well with Kotlin Multiplatform, allowing you to share data access logic while maintaining platform-specific implementations:
// commonMain/kotlin/com/example/app/repository/UserRepository.kt
interface UserRepository {
suspend fun getCurrentUser(): Result<User>
suspend fun getUserById(id: String): Result<User>
suspend fun updateProfile(profile: UserProfile): Result<User>
suspend fun logout()
}
expect class UserRepositoryFactory() {
fun create(): UserRepository
}
// Android implementation
// androidMain/kotlin/com/example/app/repository/AndroidUserRepositoryFactory.kt
actual class UserRepositoryFactory {
actual fun create(): UserRepository = AndroidUserRepository()
}
class AndroidUserRepository : UserRepository {
private val api = KtorClient.create()
private val localCache = DataStorePreferences()
override suspend fun getCurrentUser(): Result<User> = runCatching {
// Try cache first
localCache.getUser()?.let { return Result.success(it) }
// Fetch from network
val user = api.get<User>("/api/me")
localCache.saveUser(user)
user
}
// ... other implementations
}
// iOS implementation
// iosMain/kotlin/com/example/app/repository/IosUserRepositoryFactory.kt
actual class UserRepositoryFactory {
actual fun create(): UserRepository = IosUserRepository()
}
class IosUserRepository : UserRepository {
private val api = IosKtorClient()
private val keychain = KeychainManager()
override suspend fun getCurrentUser(): Result<User> = runCatching {
// iOS-specific implementation using Keychain
}
}
Use Cases
Domain use cases encapsulate business logic that can be shared across platforms:
// commonMain/kotlin/com/example/app/usecase/GetUserProfileUseCase.kt
class GetUserProfileUseCase(
private val userRepository: UserRepository,
private val settingsRepository: SettingsRepository
) {
suspend operator fun invoke(userId: String? = null): Result<UserProfile> = runCatching {
val user = userId?.let { userRepository.getUserById(it) }
?: userRepository.getCurrentUser()
.getOrThrow()
val settings = settingsRepository.getSettings()
UserProfile(
user = user,
stats = loadUserStats(user.id),
settings = settings
)
}
private suspend fun loadUserStats(userId: String): UserStats {
// Shared stats loading logic
}
}
Networking with KMP
HTTP Client Setup
Ktor Client provides excellent cross-platform networking capabilities:
// commonMain/kotlin/com/example/app/network/HttpClient.kt
fun createHttpClient(
baseUrl: String,
authToken: String? = null
): HttpClient {
return HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
})
}
install(Auth) {
authToken?.let {
bearer {
loadTokens {
// Token loading logic
}
}
}
}
install(HttpTimeout) {
requestTimeoutMillis = 30_000
connectTimeoutMillis = 15_000
socketTimeoutMillis = 30_000
}
defaultRequest {
url(baseUrl)
contentType(ContentType.Application.Json)
}
}
}
API Definition
Define your API contracts in the shared module:
// commonMain/kotlin/com/example/app/network/Api.kt
interface ApiService {
suspend fun getUser(id: String): User
suspend fun getPosts(userId: String): List<Post>
suspend fun createPost(post: CreatePostRequest): Post
suspend fun updateSettings(settings: UserSettings): UserSettings
}
class ApiServiceImpl(
private val client: HttpClient,
private val baseUrl: String
) : ApiService {
override suspend fun getUser(id: String): User {
return client.get("$baseUrl/users/$id").body()
}
override suspend fun getPosts(userId: String): List<Post> {
return client.get("$baseUrl/posts") {
parameter("userId", userId)
}.body()
}
override suspend fun createPost(post: CreatePostRequest): Post {
return client.post("$baseUrl/posts") {
setBody(post)
}.body()
}
}
Local Storage
DataStore
Jetpack DataStore provides a platform-specific but API-consistent approach to local storage:
// commonMain/kotlin/com/example/app/storage/SettingsStorage.kt
interface SettingsStorage {
suspend fun saveSettings(settings: UserSettings)
suspend fun getSettings(): UserSettings?
suspend fun clear()
}
expect class SettingsStorageFactory(): SettingsStorage
// Android implementation
actual class SettingsStorageFactory: SettingsStorage {
private val context = expectContext()
private val dataStore = context.dataStore
override suspend fun saveSettings(settings: UserSettings) {
dataStore.edit { prefs ->
prefs[PreferencesKeys.NOTIFICATIONS] = settings.notificationsEnabled
prefs[PreferencesKeys.THEME] = settings.theme.name
prefs[PreferencesKeys.LANGUAGE] = settings.language
}
}
override suspend fun getSettings(): UserSettings? {
// Implementation
}
}
// iOS implementation uses UserDefaults
SQLite with SQLDelight
SQLDelight provides type-safe SQLite access across platforms:
// commonMain/sqldelight/com/example/app/Database.sq
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL,
avatar_url TEXT,
created_at INTEGER NOT NULL,
is_premium INTEGER NOT NULL DEFAULT 0
);
getUserById:
SELECT * FROM users WHERE id = ?;
insertUser:
INSERT INTO users (id, email, name, avatar_url, created_at, is_premium)
VALUES (?, ?, ?, ?, ?, ?);
deleteUser:
DELETE FROM users WHERE id = ?;
Dependency Injection
Koin for KMP
Koin provides excellent support for Kotlin Multiplatform:
// commonMain/kotlin/com/example/app/di/AppModule.kt
val appModule = module {
// Shared dependencies
single { createHttpClient(get()) }
single { ApiServiceImpl(get(), get()) }
// Use cases
factory { GetUserProfileUseCase(get(), get()) }
factory { UpdateProfileUseCase(get()) }
// Repositories
single<UserRepository> { UserRepositoryFactory().create() }
}
// Platform-specific modules
val androidModule = module {
single { AndroidDataStore() }
viewModel { MainViewModel(get(), get()) }
}
val iosModule = module {
single { IosDataStore() }
single { MainViewModel(get(), get()) }
}
Building Native UIs
Android Implementation
Use Jetpack Compose with shared ViewModels:
// Android ViewModel
@Composable
fun UserProfileScreen(
userId: String?,
viewModel: UserProfileViewModel = koinViewModel()
) {
val state by viewModel.state.collectAsState()
when (val result = state) {
is Loading -> CircularProgressIndicator()
is Success -> UserProfileContent(result.data)
is Error -> ErrorMessage(result.message)
}
}
class UserProfileViewModel(
private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {
private val _state = MutableStateFlow<UiState>(Loading)
val state: StateFlow<UiState> = _state
fun loadProfile(userId: String?) {
viewModelScope.launch {
_state.value = Loading
_state.value = getUserProfile(userId)
.fold(
onSuccess = { Success(it) },
onFailure = { Error(it.message ?: "Unknown error") }
)
}
}
}
iOS Implementation
SwiftUI integrates seamlessly with KMP:
// iOS View using SwiftUI
import SwiftUI
import Shared
struct UserProfileView: View {
let user: UserProfile
var body: some View {
ScrollView {
VStack(spacing: 16) {
AsyncImage(url: URL(string: user.user.avatarUrl ?? "")) { image in
image.resizable()
} placeholder: {
Circle().fill(Color.gray)
}
.frame(width: 100, height: 100)
Text(user.user.name)
.font(.title)
HStack {
StatView(value: user.stats.postsCount, label: "Posts")
StatView(value: user.stats.followersCount, label: "Followers")
StatView(value: user.stats.followingCount, label: "Following")
}
}
}
}
}
Best Practices
Project Organization
Organize your shared code following clean architecture principles:
src/
โโโ commonMain/kotlin/
โ โโโ com/example/app/
โ โโโ data/ # Data layer
โ โ โโโ remote/
โ โ โโโ local/
โ โ โโโ repository/
โ โโโ domain/ # Business logic
โ โ โโโ model/
โ โ โโโ repository/
โ โ โโโ usecase/
โ โโโ di/ # Dependency injection
โโโ androidMain/
โ โโโ com/example/app/
โ โโโ ui/
โ โโโ di/
โโโ iosMain/
โโโ com/example/app/
โโโ Views/
โโโ ViewModels/
Testing Strategy
Write tests in common module for maximum coverage:
// commonTest/kotlin/com/example/app/usecase/UserUseCaseTest.kt
class GetUserProfileUseCaseTest {
@Test
fun `should return error when repository fails`() = runTest {
val mockRepo = mock<UserRepository> {
onBlocking { getCurrentUser() } returns Result.failure(
NetworkException("No connection")
)
}
val useCase = GetUserProfileUseCase(mockRepo, mockSettingsRepo)
val result = useCase()
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is NetworkException)
}
@Test
fun `should return user profile with default settings when none saved`() = runTest {
// Test implementation
}
}
Performance Considerations
Memory Management
Kotlin Native uses its own memory model different from JVM. Keep these considerations in mind:
// Use freeze() for immutable shared state
data class AppConfig(
val apiUrl: String,
val features: Set<String>
) {
fun freeze() = this
}
// Use AtomicRef for mutable state in concurrent scenarios
class Counter {
private val count = AtomicInt(0)
fun increment() = count.addAndGet(1)
fun value() = count.value
}
Startup Time
Optimize initialization for faster cold starts:
// Lazy initialization
val heavyService: HeavyService by lazy {
HeavyService()
}
// Expect initialization in platform modules
expect val initializationContext: CoroutineContext
Conclusion
Kotlin Multiplatform has established itself as a mature, production-ready solution for code sharing across platforms in 2026. By focusing on business logic rather than UI generation, KMP provides the best of both worlds: significant code reuse without sacrificing native user experiences.
The technology is particularly well-suited for teams that need to maintain multiple platforms (iOS, Android, web, desktop) while prioritizing native UX. The learning curve is manageable for teams already familiar with Kotlin, and the tooling support in Android Studio and IntelliJ IDEA makes development straightforward.
As the ecosystem continues to mature, Kotlin Multiplatform will likely become an increasingly important tool in the cross-platform development toolkit. Organizations investing in KMP today position themselves to efficiently support multiple platforms while focusing their UI efforts on platform-specific excellence.
External Resources
- Kotlin Multiplatform Documentation
- Kotlin Multiplatform Mobile
- Ktor Client Documentation
- Jetpack DataStore
- SQLDelight Documentation
- Koin Documentation
Comments