Skip to main content
โšก Calmops

Kotlin Multiplatform 2026 Complete Guide: Share Code Across Platforms

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

Comments