Skip to main content

Kotlin Multiplatform: Share Code Across Platforms

Created: February 27, 2026 Larry Qu 6 min read

Kotlin Multiplatform (KMP) enables sharing Kotlin code between different platforms while keeping platform-specific implementations where needed. This comprehensive guide covers everything you need to know about building cross-platform applications with Kotlin.

What is Kotlin Multiplatform?

Kotlin Multiplatform allows you to write Kotlin code that compiles to multiple platforms.

// Shared code - works on all platforms
fun greet(name: String): String {
    return "Hello, $name!"
}

// Platform-specific code
expect fun getPlatformName(): String

actual fun getPlatformName(): String = "iOS" // iOS implementation

actual fun getPlatformName(): String = "Android" // Android implementation

Key Features

  • Code Sharing - Share business logic across platforms
    • Native Performance - Compiles to native code
    • Platform APIs - Access platform-specific features
    • Flexible - Choose what to share
    • Interoperate - Works with existing native code

Project Setup

Gradle Configuration

// build.gradle.kts (project level)
plugins {
    kotlin("multiplatform") version "1.9.20" apply false
    kotlin("android") version "1.9.20" apply false
    kotlin("iosX64") version "1.9.20" apply false
    kotlin("iosArm64") version "1.9.20" apply false
    id("com.android.application") version "8.2.0" apply false
}

// build.gradle.kts (module level)
plugins {
    kotlin("multiplatform")
    id("com.android.application")
}

kotlin {
    android {
        compilations.all {
            kotlinOptions {
                jvmTarget = "17"
            }
        }
    }
    
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    
    sourceSets {
        val commonMain by getting
        val androidMain by getting
        val iosMain by getting
    }
}

Project Structure

my-kmp-app/
├── build.gradle.kts
├── src/
│   ├── commonMain/
│   │   └── kotlin/
│   │       └── com/example/
│   │           ├── Greeting.kt
│   │           └── Platform.kt
│   ├── androidMain/
│   │   └── kotlin/
│   │       └── com/example/
│   │           └── Platform.android.kt
│   └── iosMain/
│       └── kotlin/
│           └── com/example/
│               └── Platform.ios.kt
├── androidApp/
└── iosApp/

Expect/Actual Pattern

Basic Usage

// commonMain/kotlin/Platform.kt
expect class Platform() {
    val name: String
}

// androidMain/kotlin/Platform.android.kt
actual class Platform actual() {
    actual val name: String = "Android"
}

// iosMain/kotlin/Platform.ios.kt
actual class Platform actual() {
    actual val name: String = "iOS"
}

Functions

// commonMain/kotlin/Logger.kt
expect fun log(message: String)

// androidMain/kotlin/Logger.android.kt
actual fun log(message: String) {
    android.util.Log.d("KMP", message)
}

// iosMain/kotlin/Logger.ios.kt
actual fun log(message: String) {
    println("KMP: $message")
}

Properties

// commonMain/kotlin/Config.kt
expect val appName: String

// androidMain/kotlin/Config.android.kt
actual val appName: String = "MyKMPApp"

// iosMain/kotlin/Config.ios.kt
actual val appName: String = "MyKMPApp"

Sharing Code

Data Classes

// Shared data models
@Serializable
data class User(
    val id: String,
    val name: String,
    val email: String,
    val avatarUrl: String? = null
)

@Serializable
data class Post(
    val id: String,
    val authorId: String,
    val title: String,
    val content: String,
    val createdAt: Long
)

Business Logic

// commonMain/kotlin/Validator.kt
object Validator {
    fun validateEmail(email: String): Boolean {
        return email.isNotBlank() && 
               email.contains("@") && 
               email.contains(".")
    }
    
    fun validatePassword(password: String): ValidationResult {
        return when {
            password.length < 8 -> 
                ValidationResult.Invalid("Password too short")
            !password.any { it.isDigit() } -> 
                ValidationResult.Invalid("Password needs a number")
            else -> ValidationResult.Valid
        }
    }
    
    sealed class ValidationResult {
        object Valid : ValidationResult()
        data class Invalid(val message: String) : ValidationResult()
    }
}

Use Cases

// commonMain/kotlin/usecase/GetUserUseCase.kt
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(userId: String): Result<User> {
        return try {
            val user = repository.getUser(userId)
            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Platform-Specific Code

Android Implementation

// androidMain/kotlin/repository/UserRepository.android.kt
class UserRepositoryAndroid(
    private val context: Context
) : UserRepository {
    
    private val prefs = context.getSharedPreferences("users", Context.MODE_PRIVATE)
    
    override suspend fun getUser(id: String): User {
        val json = prefs.getString(id, null)
        return if (json != null) {
            Json.decodeFromString<User>(json)
        } else {
            throw UserNotFoundException(id)
        }
    }
    
    override suspend fun saveUser(user: User) {
        prefs.edit()
            .putString(user.id, Json.encodeToString(user))
            .apply()
    }
}

iOS Implementation

// iosMain/kotlin/repository/UserRepository.ios.kt
class UserRepositoryIOS : UserRepository {
    
    private val userDefaults = NSUserDefaults.standardUserDefaults
    
    override suspend fun getUser(id: String): User? {
        val data = userDefaults.dataForKey(id)
        return data?.let {
            Json.decodeFromString<User>(it.toString())
        }
    }
    
    override suspend fun saveUser(user: User) {
        let data = Json.encodeToString(user)
        userDefaults.set(data, forKey: user.id)
    }
}

Serialization

JSON Serialization

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

val json = Json { 
    prettyPrint = true 
    ignoreUnknownKeys = true
}

val user = User("1", "John", "[email protected]")
val jsonString = json.encodeToString(user)
val decoded = json.decodeFromString<User>(jsonString)

Collections

@Serializable
data class ApiResponse<T>(
    val data: List<T>,
    val total: Int,
    val page: Int
)

val response = json.decodeFromString<ApiResponse<User>>(
    """{"data":[],"total":0,"page":1}"""
)

Networking

HttpClient

// commonMain/kotlin/network/HttpClient.kt
expect fun createHttpClient(): HttpClient

// Using Ktor client
actual fun createHttpClient() = HttpClient {
    install(ContentNegotiation) {
        json()
    }
    install(HttpTimeout) {
        requestTimeoutMillis = 30000
    }
}

// Usage in shared code
suspend fun fetchUsers(): List<User> {
    val client = createHttpClient()
    return client.get("https://api.example.com/users")
        .body()
}

Ktor Client

class UserApi(private val client: HttpClient) {
    
    suspend fun getUsers(): List<User> {
        return client.get("https://api.example.com/users")
            .body()
    }
    
    suspend fun getUser(id: String): User {
        return client.get("https://api.example.com/users/$id")
            .body()
    }
    
    suspend fun createUser(user: User): User {
        return client.post("https://api.example.com/users") {
            setBody(user)
        }.body()
    }
}

Dependencies

Common Dependencies

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
                implementation("io.ktor:ktor-client-core:2.3.6")
            }
        }
    }
}

Platform Dependencies

kotlin {
    sourceSets {
        val androidMain by getting {
            dependencies {
                implementation("androidx.core:core-ktx:1.12.0")
            }
        }
        
        val iosMain by getting {
            dependencies {
                // iOS-specific dependencies
            }
        }
    }
}

Architecture

Clean Architecture

src/
├── commonMain/kotlin/
│   └── com/example/
│       ├── domain/
│       │   ├── model/
│       │   ├── repository/
│       │   │   └── UserRepository.kt (expect)
│       │   └── usecase/
│       ├── data/
│       │   └── repository/
│       │       └── UserRepositoryImpl.kt
│       └── presentation/
│           └── viewmodel/
├── androidMain/
│   └── ...
└── iosMain/
    └── ...

ViewModel

// Common state
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

// Common ViewModel
expect class ViewModel()

actual class ViewModel actual() : androidx.lifecycle.ViewModel() {
    // Android implementation
}

// Usage
class UserListViewModel(
    private val getUsersUseCase: GetUsersUseCase
) : ViewModel() {
    
    private val _state = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
    val state: StateFlow<UiState<List<User>>> = _state
    
    fun loadUsers() {
        _state.value = UiState.Loading
        // Load users...
    }
}

Native UI

Compose Multiplatform

// build.gradle.kts
plugins {
    kotlin("plugin.compose") version "1.9.20"
}

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material3)
            }
        }
    }
}

// Shared UI
@Composable
fun UserCard(user: User) {
    Card {
        Column {
            Text(user.name)
            Text(user.email)
        }
    }
}

Testing

Common Tests

class ValidatorTest {
    @Test
    fun `validateEmail with valid email returns true`() {
        val result = Validator.validateEmail("[email protected]")
        assertTrue(result)
    }
    
    @Test
    fun `validateEmail with invalid email returns false`() {
        val result = Validator.validateEmail("invalid")
        assertFalse(result)
    }
}

External Resources

Conclusion

Kotlin Multiplatform enables efficient code sharing across platforms. Key points:

  • Use expect/actual for platform-specific code
  • Share business logic, data models, and use cases
  • Keep UI platform-specific or use Compose Multiplatform
  • Access platform APIs through actual implementations
  • Test shared code once, run everywhere

KMP is ideal for teams building apps on multiple platforms who want to maximize code reuse while maintaining native user experiences.

Resources

Comments

Share this article

Scan to read on mobile

👍 Was this article helpful?