Kotlin Multiplatform Projects
Introduction
Kotlin Multiplatform (KMP) is a powerful technology that allows developers to share code across different platforms like Android, iOS, web, desktop, and more, while still maintaining the flexibility to use platform-specific APIs when needed. It's a pragmatic approach to cross-platform development that focuses on maximizing code sharing while preserving native performance and user experience.
In this guide, we'll explore how Kotlin Multiplatform works, how to set up a basic multiplatform project, and how to write code that can be shared across platforms while also leveraging platform-specific capabilities.
Understanding Kotlin Multiplatform
Kotlin Multiplatform is built on the principle of sharing business logic rather than UI code. This approach acknowledges that each platform has its own UI paradigms and best practices, while much of the underlying business logic can be identical.
Key Benefits of Kotlin Multiplatform
- Code Sharing: Write core business logic, networking, data processing, and other non-UI code once
- Native Performance: Compile to platform-specific binaries for optimal performance
- Interoperability: Seamlessly interoperate with platform-specific code (Swift/Obj-C for iOS, Java for Android)
- Gradual Adoption: Can be integrated into existing projects incrementally
- Single Language: Use Kotlin for all platforms rather than switching between different languages
Setting Up a Kotlin Multiplatform Project
Let's start by creating a simple Kotlin Multiplatform project that targets Android and iOS.
Prerequisites
- IntelliJ IDEA or Android Studio
- Kotlin plugin installed (1.5.0 or higher recommended)
- For iOS development: macOS with Xcode installed
Creating a New Multiplatform Project
- Open IntelliJ IDEA or Android Studio
- Select "New Project"
- Choose "Kotlin Multiplatform" and select "Mobile (Android/iOS)" template
- Configure your project name and other details, then click "Finish"
The generated project will have the following structure:
my-kmp-project/
├── androidApp/ // Android-specific code
├── iosApp/ // iOS-specific code
├── shared/ // Shared code for both platforms
│ ├── build.gradle.kts // Shared module build configuration
│ └── src/
│ ├── androidMain/ // Android-specific implementations
│ ├── commonMain/ // Shared code for all platforms
│ └── iosMain/ // iOS-specific implementations
└── build.gradle.kts // Root build configuration
Writing Shared Code
Let's create a simple calculator module that we can share between platforms.
Common Code
First, let's create our calculator logic in the shared module:
// shared/src/commonMain/kotlin/com/example/calculator/Calculator.kt
package com.example.calculator
class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun subtract(a: Int, b: Int): Int = a - b
fun multiply(a: Int, b: Int): Int = a * b
fun divide(a: Int, b: Int): Int {
if (b == 0) throw IllegalArgumentException("Cannot divide by zero")
return a / b
}
}
This simple calculator class will work on all platforms because it uses only the common subset of Kotlin functionality.
Platform-Specific Implementations
Sometimes we need to implement functionality differently on each platform. Kotlin Multiplatform handles this through the expect/actual
pattern:
// shared/src/commonMain/kotlin/com/example/calculator/PlatformUtils.kt
package com.example.calculator
expect class PlatformUtils() {
fun getPlatformName(): String
fun formatCurrency(amount: Double): String
}
Now we need to provide actual implementations for each platform:
// shared/src/androidMain/kotlin/com/example/calculator/PlatformUtils.kt
package com.example.calculator
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale
actual class PlatformUtils {
actual fun getPlatformName(): String = "Android"
actual fun formatCurrency(amount: Double): String {
val format = NumberFormat.getCurrencyInstance(Locale.US)
format.currency = Currency.getInstance("USD")
return format.format(amount)
}
}
// shared/src/iosMain/kotlin/com/example/calculator/PlatformUtils.kt
package com.example.calculator
import platform.Foundation.NSLocale
import platform.Foundation.NSNumber
import platform.Foundation.NSNumberFormatter
import platform.Foundation.NSNumberFormatterCurrencyStyle
actual class PlatformUtils {
actual fun getPlatformName(): String = "iOS"
actual fun formatCurrency(amount: Double): String {
val formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterCurrencyStyle
formatter.locale = NSLocale.localeWithLocaleIdentifier("en_US")
return formatter.stringFromNumber(NSNumber(amount)) ?: "$amount"
}
}
Using Multiplatform Libraries
Kotlin Multiplatform has a growing ecosystem of libraries. Let's add a popular multiplatform networking library called Ktor to our project:
Add the following to your shared/build.gradle.kts
:
// shared/build.gradle.kts
kotlin {
// ... other configuration
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:2.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:2.3.2")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.2")
}
}
}
}
// Enable serialization plugin
plugins {
kotlin("plugin.serialization") version "1.8.20"
}
Now let's create a simple API client:
// shared/src/commonMain/kotlin/com/example/calculator/api/ApiClient.kt
package com.example.calculator.api
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Todo(
val id: Int,
val title: String,
val completed: Boolean
)
class TodoApi {
private val client = HttpClient()
private val json = Json { ignoreUnknownKeys = true }
suspend fun getTodo(id: Int): Todo {
val response = client.get("https://jsonplaceholder.typicode.com/todos/$id")
val todoJson = response.bodyAsText()
return json.decodeFromString(todoJson)
}
}
Using Shared Code in Android
Let's use our shared code in an Android application:
// androidApp/src/main/java/com/example/calculator/android/MainActivity.kt
package com.example.calculator.android
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.calculator.Calculator
import com.example.calculator.PlatformUtils
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val calculator = Calculator()
val platformUtils = PlatformUtils()
setContent {
var result by remember { mutableStateOf("") }
val coroutineScope = rememberCoroutineScope()
MaterialTheme {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Running on: ${platformUtils.getPlatformName()}")
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
val sum = calculator.add(5, 3)
result = "5 + 3 = $sum"
}
) {
Text("Calculate 5 + 3")
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
result = "Formatted: ${platformUtils.formatCurrency(125.99)}"
}
) {
Text("Format Currency")
}
Spacer(modifier = Modifier.height(16.dp))
Text(result)
}
}
}
}
}
Using Shared Code in iOS
For iOS, you'll use Swift to interact with your Kotlin shared code:
// iosApp/iosApp/ContentView.swift
import SwiftUI
import shared
struct ContentView: View {
@State private var result: String = ""
let calculator = Calculator()
let platformUtils = PlatformUtils()
var body: some View {
VStack(spacing: 20) {
Text("Running on: \(platformUtils.getPlatformName())")
Button("Calculate 5 + 3") {
let sum = calculator.add(a: 5, b: 3)
result = "5 + 3 = \(sum)"
}
.padding()
Button("Format Currency") {
result = "Formatted: \(platformUtils.formatCurrency(amount: 125.99))"
}
.padding()
Text(result)
.padding()
}
}
}
Working with Concurrency
Since Android and iOS handle concurrency differently, Kotlin Multiplatform provides a unified way to work with coroutines across platforms:
// shared/src/commonMain/kotlin/com/example/calculator/Repository.kt
package com.example.calculator
import com.example.calculator.api.TodoApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class TodoRepository {
private val api = TodoApi()
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val _todoTitle = MutableStateFlow<String?>(null)
val todoTitle = _todoTitle.asStateFlow()
fun fetchTodo(id: Int) {
coroutineScope.launch {
try {
val todo = api.getTodo(id)
_todoTitle.value = todo.title
} catch (e: Exception) {
_todoTitle.value = "Error: ${e.message}"
}
}
}
}
Common Challenges in Multiplatform Projects
1. Handling Data Persistence
For data storage, you might use different approaches:
// shared/src/commonMain/kotlin/com/example/calculator/storage/Storage.kt
package com.example.calculator.storage
expect class Storage() {
fun saveString(key: String, value: String)
fun getString(key: String): String?
}
// Then implement platform-specific versions using SharedPreferences for Android
// and UserDefaults for iOS in their respective source sets
2. Dependency Injection
For dependency injection in multiplatform projects, you can use Koin:
// Add to shared/build.gradle.kts in commonMain dependencies
implementation("io.insert-koin:koin-core:3.4.0")
// shared/src/commonMain/kotlin/com/example/calculator/di/AppModule.kt
package com.example.calculator.di
import com.example.calculator.Calculator
import com.example.calculator.TodoRepository
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module
fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin {
appDeclaration()
modules(commonModule())
}
fun commonModule() = module {
single { Calculator() }
single { TodoRepository() }
}
Real-World Example: Currency Converter
Let's build a simple currency converter that demonstrates a more complete example:
// shared/src/commonMain/kotlin/com/example/calculator/currency/CurrencyConverter.kt
package com.example.calculator.currency
import com.example.calculator.PlatformUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class CurrencyConverter {
private val rates = mapOf(
"USD" to 1.0,
"EUR" to 0.92,
"GBP" to 0.79,
"JPY" to 149.58,
"CAD" to 1.36
)
private val platformUtils = PlatformUtils()
private val _convertedAmount = MutableStateFlow<String?>(null)
val convertedAmount: StateFlow<String?> = _convertedAmount.asStateFlow()
fun convert(amount: Double, fromCurrency: String, toCurrency: String) {
val fromRate = rates[fromCurrency] ?: return
val toRate = rates[toCurrency] ?: return
val convertedValue = amount * (toRate / fromRate)
_convertedAmount.value = platformUtils.formatCurrency(convertedValue)
}
fun availableCurrencies(): List<String> = rates.keys.toList()
}
Summary
Kotlin Multiplatform offers a pragmatic approach to cross-platform development by allowing you to share business logic across platforms while using platform-specific UI frameworks. This gives you the best of both worlds: code sharing and native user experience.
Key takeaways:
- Use the common source set for shared code
- Use
expect/actual
pattern for platform-specific implementations - Leverage multiplatform libraries like Ktor, SQLDelight, and Koin
- Kotlin coroutines work seamlessly across platforms
Kotlin Multiplatform is particularly suited for teams that:
- Already have experience with Kotlin
- Need to support multiple platforms
- Want to maximize code sharing without sacrificing native look and feel
- Are looking for a gradual adoption path for cross-platform development
Additional Resources
- Official Kotlin Multiplatform Documentation
- Kotlin Multiplatform Mobile Developer Portal
- KaMPKit - A starter kit for Kotlin Multiplatform
- Kotlin Multiplatform Samples
Exercises
- Create a simple note-taking application with shared data models and storage logic
- Implement a weather app using a multiplatform API client
- Add dependency injection to your multiplatform project using Koin
- Create a simple game with shared game logic but platform-specific UI
- Integrate SQLDelight for database operations in a multiplatform project
By mastering Kotlin Multiplatform, you'll be able to leverage your Kotlin skills across multiple platforms, reducing duplication and maintenance costs while still providing the best native experience to users.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)