Skip to main content

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

  1. Open IntelliJ IDEA or Android Studio
  2. Select "New Project"
  3. Choose "Kotlin Multiplatform" and select "Mobile (Android/iOS)" template
  4. 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:

kotlin
// 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:

kotlin
// 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:

kotlin
// 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)
}
}
kotlin
// 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:

kotlin
// 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:

kotlin
// 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:

kotlin
// 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:

swift
// 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:

kotlin
// 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:

kotlin
// 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:

kotlin
// 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:

kotlin
// 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

Exercises

  1. Create a simple note-taking application with shared data models and storage logic
  2. Implement a weather app using a multiplatform API client
  3. Add dependency injection to your multiplatform project using Koin
  4. Create a simple game with shared game logic but platform-specific UI
  5. 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! :)