Skip to main content
โšก Calmops

Kotlin Multiplatform: Share Code Across Platforms

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.

Comments