Skip to content

Kotlin Multiplatform Basics — Shared Code Across Platforms

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Kotlin Multiplatform Basics. We cover key concepts, practical examples, and best practices to help you master this topic.

Kotlin Multiplatform (KMP) compiles shared Kotlin code to JVM bytecode, JavaScript, and native binaries, enabling common business logic across Android, iOS, web, and desktop with platform-specific implementations through expect/actual declarations.

What You'll Learn

  • Structure a KMP project with shared and platform modules
  • Use expect/actual for platform-specific code
  • Share business logic between Android and iOS
  • Use Kotlin Multiplatform for networking and data storage
  • Integrate with platform-specific frameworks (UIKit, SwiftUI)
  • Test shared code across platforms
  • Understand KMP limitations and compatibility

Why It Matters

KMP solves the problem of writing the same business logic twice: once for Android (Kotlin) and once for iOS (Swift). With KMP, you write the shared logic once in Kotlin and compile it for both platforms. This reduces development time, ensures consistent behavior, and simplifies maintenance. Companies like Netflix, McDonald's, and Quizlet use KMP in production.

Real-World Use

DodaTech uses KMP to share network layer, data validation, and malware signature processing across Android and iOS apps. The shared module compiles to both platforms. Platform-specific UI is written in Jetpack Compose (Android) and SwiftUI (iOS). This approach reduced code duplication by 60%.

Learning Path

flowchart LR
  A[Flow] --> B[KMP Basics\nYou are here]
  B --> C[Ktor]
  style B fill:#f90,color:#fff

Project Structure

A KMP project has three modules: shared, androidApp, and iosApp.

MyKmpProject/
├── shared/
│   ├── src/
│   │   ├── commonMain/     # Shared code
│   │   ├── androidMain/    # Android-specific
│   │   └── iosMain/        # iOS-specific
│   └── build.gradle.kts
├── androidApp/
│   └── src/
├── iosApp/
│   └── iosApp.xcodeproj
├── build.gradle.kts
└── settings.gradle.kts

Build Configuration

// settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolution {
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "MyKmpProject"
include(":shared")
include(":androidApp")
// shared/build.gradle.kts
plugins {
    kotlin("multiplatform")
    id("com.android.library")
    kotlin("plugin.serialization") version "2.0.21"
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "17"
            }
        }
    }
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
    }
    
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
                implementation("io.ktor:ktor-client-core:2.3.12")
            }
        }
        
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-android:2.3.12")
            }
        }
        
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
            dependencies {
                implementation("io.ktor:ktor-client-darwin:2.3.12")
            }
        }
    }
}

android {
    namespace = "com.example.shared"
    compileSdk = 35
    defaultConfig {
        minSdk = 24
    }
}

Shared Module Code

Write common business logic in commonMain.

// shared/src/commonMain/kotlin/com/example/shared/Greeting.kt
package com.example.shared

class Greeting {
    private val platform = getPlatform()
    
    fun greet(): String {
        return "Hello from Kotlin on ${platform.name}!"
    }
    
    fun formattedDate(): String {
        return "Today is ${PlatformUtils.currentDate()}"
    }
}

Expect/Actual Declarations

Use expect in commonMain and provide actual implementations per platform.

// shared/src/commonMain/kotlin/com/example/shared/Platform.kt
package com.example.shared

expect class Platform() {
    val name: String
}

expect object PlatformUtils {
    fun currentDate(): String
}

expect fun randomUUID(): String

Android Implementation

// shared/src/androidMain/kotlin/com/example/shared/Platform.android.kt
package com.example.shared

import java.util.UUID
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

actual class Platform actual constructor() {
    actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

actual object PlatformUtils {
    actual fun currentDate(): String {
        val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
        return formatter.format(Date())
    }
}

actual fun randomUUID(): String = UUID.randomUUID().toString()

iOS Implementation

// shared/src/iosMain/kotlin/com/example/shared/Platform.ios.kt
package com.example.shared

import platform.Foundation.NSDate
import platform.Foundation.NSDateFormatter
import platform.Foundation.NSUUID

actual class Platform actual constructor() {
    actual val name: String = "iOS ${platform.UIKit.UIDevice.currentDevice.systemVersion}"
}

actual object PlatformUtils {
    actual fun currentDate(): String {
        val formatter = NSDateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter.stringFromDate(NSDate())
    }
}

actual fun randomUUID(): String = NSUUID().UUIDString()

Networking with Ktor in Shared Module

// shared/src/commonMain/kotlin/com/example/shared/network/ApiClient.kt
package com.example.shared.network

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

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

class ApiClient {
    private val httpClient = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                isLenient = true
            })
        }
    }
    
    suspend fun getUsers(): List<User> {
        return httpClient.get("https://jsonplaceholder.typicode.com/users").body()
    }
    
    suspend fun getUser(id: Int): User {
        return httpClient.get("https://jsonplaceholder.typicode.com/users/$id").body()
    }
}

Data Storage with Settings

Use multiplatform-settings for key-value storage.

// shared/src/commonMain/kotlin/com/example/shared/storage/AppSettings.kt
package com.example.shared.storage

expect class SettingsFactory {
    fun createSettings(): Settings
}

interface Settings {
    fun getString(key: String, default: String = ""): String
    fun putString(key: String, value: String)
    fun getBoolean(key: String, default: Boolean = false): Boolean
    fun putBoolean(key: String, value: Boolean)
    fun remove(key: String)
    fun clear()
}

Using Shared Module from Android

// androidApp/src/main/kotlin/.../MainActivity.kt
import com.example.shared.Greeting
import com.example.shared.network.ApiClient

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val greeting = Greeting()
        println(greeting.greet())
        
        // Use shared API client
        CoroutineScope(Dispatchers.IO).launch {
            val api = ApiClient()
            val users = api.getUsers()
            users.forEach { println("${it.name}: ${it.email}") }
        }
    }
}

Using Shared Module from iOS (Swift)

// iosApp/ContentView.swift
import SwiftUI
import shared

struct ContentView: View {
    let greeting = Greeting()
    
    var body: some View {
        Text(greeting.greet())
            .padding()
    }
}

The shared framework is generated as an iOS framework that Swift can import directly.

Testing Shared Code

Test commonMain code with multiplatform tests.

// shared/src/commonTest/kotlin/com/example/shared/GreetingTest.kt
package com.example.shared

import kotlin.test.Test
import kotlin.test.assertTrue

class GreetingTest {
    @Test
    fun testGreeting() {
        val greeting = Greeting()
        val result = greeting.greet()
        assertTrue(result.contains("Hello from Kotlin"))
    }
}

Run tests with ./gradlew :shared:allTests.

Common Mistakes

  1. Putting platform-specific code in commonMain: commonMain code must compile to all platforms. Use expect/actual for any platform-specific functionality.

  2. Not using a dependency injection pattern: Platform-specific implementations of expect declarations require careful management. Use a DI framework or manual Factory pattern.

  3. Ignoring iOS memory management: Kotlin/Native uses automatic memory management but references are different from JVM. Watch for memory leaks from captured references.

  4. Using Java libraries in common code: JVM-specific libraries (like java.time) are not available on iOS. Use Kotlin multiplatform libraries (kotlinx-datetime) instead.

  5. Not handling Objective-C interop correctly: iOS interop uses Objective-C bridging. Collection types, nullability, and naming conventions differ from Swift.

  6. Forgetting to regenerate the iOS framework: After changing shared code, rebuild the framework with ./gradlew :shared:linkDebugFrameworkIosArm64.

Practice Questions

  1. What is the purpose of expect/actual declarations in KMP?

Answer: expect declares an API in commonMain. actual provides the platform-specific implementation in each platform source set.

  1. How does KMP compile to iOS?

Answer: Kotlin/Native compiles shared Kotlin code to a native iOS framework (static or dynamic) using LLVM. Swift and Objective-C code can import this framework.

  1. What libraries work across all KMP targets?

Answer: kotlinx-coroutines, kotlinx-Serialization, ktor-client, kotlinx-datetime, and multiplatform-settings work across Android, iOS, and desktop.

  1. What is the shared module's role in a KMP project?

Answer: The shared module contains common business logic, data models, networking, and validation code. Platform-specific modules (androidApp, iosApp) build on top of it.

  1. Challenge: Create a KMP module that validates email addresses and passwords. The validation logic is shared. The Android app shows results in Compose. The iOS app shows results in SwiftUI.

Answer:

// shared/src/commonMain/kotlin/.../validation/Validator.kt
object Validator {
    fun isValidEmail(email: String): Boolean {
        return email.contains("@") && email.contains(".") && email.length > 5
    }
    
    fun isValidPassword(password: String): ValidationResult {
        return when {
            password.length < 8 -> ValidationResult.Weak("Too short")
            !password.any { it.isDigit() } -> ValidationResult.Weak("No digits")
            !password.any { it.isUpperCase() } -> ValidationResult.Medium("Add uppercase")
            !password.any { it.isLowerCase() } -> ValidationResult.Medium("Add lowercase")
            else -> ValidationResult.Strong
        }
    }
}

sealed class ValidationResult {
    data object Strong : ValidationResult()
    data class Medium(val suggestion: String) : ValidationResult()
    data class Weak(val reason: String) : ValidationResult()
}

Mini Project

Build a KMP login module that validates credentials, authenticates via an API, and stores the auth token. Requirements:

  • Shared validation logic for email and password
  • Shared API client using Ktor
  • Shared token storage using expect/actual
  • Android app with Compose UI
  • iOS app with SwiftUI
  • Test the shared validation and API client

This project demonstrates full KMP development from shared logic to platform-specific UI.

FAQ

Can I use KMP for sharing UI?

KMP shares business logic, not UI. For shared UI, use Compose Multiplatform which targets Android, iOS, and desktop.

What is the difference between KMP and Flutter?

KMP lets you share Kotlin logic while using native UI (Compose/SwiftUI). Flutter uses Dart for both logic and UI. KMP integrates better with existing native codebases.

Can I use KMP with existing Swift projects?

Yes. Generate a Kotlin/Native framework and add it as a dependency in your Xcode project. The framework exposes Kotlin APIs as Objective-C-compatible interfaces.

Does KMP support desktop platforms?

Yes. KMP targets JVM (Windows, Linux, macOS) and native (macOS). JetBrains actively develops desktop support.

What is the performance impact of KMP?

Kotlin/Native compiles to machine code with performance comparable to Swift. The shared module adds minimal overhead.

What's Next

After learning KMP basics, explore Ktor for server-side development. You can also learn testing for shared code quality.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro