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
- Kotlin Multiplatform Documentation
- Kotlin Multiplatform Mobile
- KMP Samples
- Jetpack Compose Multiplatform
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