Skip to main content

Kotlin Common Code

Introduction

Kotlin Multiplatform is a powerful technology that allows developers to share code between different platforms, including Android, iOS, web, and desktop applications. At the heart of this technology is the concept of Kotlin Common Code - the shared codebase that works across all target platforms.

In this tutorial, we'll explore how to write, organize, and maintain common code in Kotlin Multiplatform projects. You'll learn how to maximize code sharing while respecting platform-specific requirements, resulting in more efficient development and easier maintenance.

What is Kotlin Common Code?

Kotlin Common Code refers to the platform-independent code written in Kotlin that can be compiled and run on any supported platform. This code is typically placed in a special source set called commonMain in Kotlin Multiplatform projects.

The key benefits of writing common code include:

  • Reduced duplication: Write business logic, algorithms, and data models once instead of implementing them for each platform
  • Consistent behavior: Ensure all platforms behave the same way for core functionality
  • Easier maintenance: Bug fixes and feature updates need to be implemented only once
  • Faster development: Less code to write and test

Setting Up a Multiplatform Project Structure

Let's look at a typical Kotlin Multiplatform project structure:

src/
├── commonMain/ # Common code for all platforms
│ ├── kotlin/ # Kotlin source files
│ └── resources/ # Common resources
├── androidMain/ # Android-specific code
│ ├── kotlin/
│ └── resources/
├── iosMain/ # iOS-specific code
│ ├── kotlin/
│ └── resources/
└── jvmMain/ # JVM-specific code
├── kotlin/
└── resources/

The commonMain directory is where all your shared code will live. Each platform-specific directory contains code that will only be compiled for that particular platform.

Writing Your First Common Code

Let's start with a simple example of common code:

kotlin
// In commonMain/kotlin/com/example/Person.kt
package com.example

class Person(val name: String, val age: Int) {
fun isAdult(): Boolean {
return age >= 18
}

override fun toString(): String {
return "Person(name=$name, age=$age)"
}
}

This Person class can be used across all platforms without any modifications. Now let's write a function to greet a person:

kotlin
// In commonMain/kotlin/com/example/Greeting.kt
package com.example

fun greet(person: Person): String {
return "Hello, ${person.name}! " +
if (person.isAdult()) "Welcome!" else "You must be accompanied by an adult."
}

Working with Platform-Specific Code

Sometimes you'll need functionality that varies across platforms. Kotlin Multiplatform handles this through expect/actual declarations.

Using expect/actual Declarations

In your common code, you define an expected declaration:

kotlin
// In commonMain/kotlin/com/example/Platform.kt
package com.example

expect class Platform() {
val name: String
fun getVersion(): String
}

expect fun getPlatformGreeting(): String

Then you provide actual implementations in each platform-specific source set:

kotlin
// In androidMain/kotlin/com/example/Platform.kt
package com.example

actual class Platform actual constructor() {
actual val name: String = "Android"
actual fun getVersion(): String = android.os.Build.VERSION.RELEASE
}

actual fun getPlatformGreeting(): String = "Hello from Android!"
kotlin
// In iosMain/kotlin/com/example/Platform.kt
package com.example

import platform.UIKit.UIDevice

actual class Platform actual constructor() {
actual val name: String = "iOS"
actual fun getVersion(): String = UIDevice.currentDevice.systemVersion
}

actual fun getPlatformGreeting(): String = "Hello from iOS!"
kotlin
// In jvmMain/kotlin/com/example/Platform.kt
package com.example

actual class Platform actual constructor() {
actual val name: String = "JVM"
actual fun getVersion(): String = System.getProperty("java.version")
}

actual fun getPlatformGreeting(): String = "Hello from JVM!"

Using the Platform-Specific Code in Common Code

Now you can use these platform-specific implementations in your common code:

kotlin
// In commonMain/kotlin/com/example/App.kt
package com.example

fun appInfo(): String {
val platform = Platform()
return "Running on ${platform.name} ${platform.getVersion()}"
}

fun welcome(): String {
return getPlatformGreeting() + " Welcome to our Kotlin Multiplatform app!"
}

Accessing Platform Libraries from Common Code

Sometimes you need to use platform-specific libraries in your common code. Here's how to do it:

Using Interfaces

Define interfaces in common code, and implement them for each platform:

kotlin
// In commonMain/kotlin/com/example/Logger.kt
package com.example

interface Logger {
fun debug(message: String)
fun info(message: String)
fun error(message: String, throwable: Throwable? = null)
}

// In commonMain/kotlin/com/example/LoggerProvider.kt
package com.example

expect object LoggerProvider {
fun createLogger(tag: String): Logger
}

Implement platform-specific versions:

kotlin
// In androidMain/kotlin/com/example/LoggerProvider.kt
package com.example

import android.util.Log

actual object LoggerProvider {
actual fun createLogger(tag: String): Logger = AndroidLogger(tag)

private class AndroidLogger(private val tag: String) : Logger {
override fun debug(message: String) {
Log.d(tag, message)
}

override fun info(message: String) {
Log.i(tag, message)
}

override fun error(message: String, throwable: Throwable?) {
if (throwable != null) {
Log.e(tag, message, throwable)
} else {
Log.e(tag, message)
}
}
}
}

Real-World Example: Networking Client

Let's create a more practical example - a networking client that works across platforms:

kotlin
// In commonMain/kotlin/com/example/networking/HttpClient.kt
package com.example.networking

expect class HttpClient() {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}

// In commonMain/kotlin/com/example/networking/UserApi.kt
package com.example.networking

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

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

class UserApi {
private val httpClient = HttpClient()
private val baseUrl = "https://api.example.com/users"
private val json = Json { ignoreUnknownKeys = true }

suspend fun getUser(id: Int): User {
val response = httpClient.get("$baseUrl/$id")
return json.decodeFromString(response)
}

suspend fun createUser(user: User): User {
val body = json.encodeToString(user)
val response = httpClient.post(baseUrl, body)
return json.decodeFromString(response)
}
}

Then implement the HttpClient for each platform:

kotlin
// In androidMain/kotlin/com/example/networking/HttpClient.kt
package com.example.networking

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL

actual class HttpClient actual constructor() {
actual suspend fun get(url: String): String = withContext(Dispatchers.IO) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.inputStream.bufferedReader().use { it.readText() }
}

actual suspend fun post(url: String, body: String): String = withContext(Dispatchers.IO) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")

connection.outputStream.use { os ->
os.write(body.toByteArray())
os.flush()
}

connection.inputStream.bufferedReader().use { it.readText() }
}
}

Best Practices for Common Code

To make the most of Kotlin Common Code, follow these best practices:

  1. Maximize code sharing: Try to put as much logic as possible in the common module
  2. Use interfaces for platform-specific functionality: Define interfaces in common code and implement them per platform
  3. Leverage multiplatform libraries: Use libraries like Ktor, SQLDelight, and kotlinx.serialization that are designed for multiplatform
  4. Keep platform-specific code minimal: Only use platform-specific code when absolutely necessary
  5. Use expect/actual wisely: Don't overuse it; consider if an interface-based approach would be cleaner
  6. Test common code thoroughly: Write tests for your common code to ensure it works correctly across platforms

Dependencies in Common Code

You can use Kotlin Multiplatform libraries in your common code. Here's how to add dependencies in your build.gradle.kts file:

kotlin
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.4")
}
}

val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:2.3.4")
}
}

val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-ios:2.3.4")
}
}
}
}

Summary

Kotlin Common Code is the foundation of Kotlin Multiplatform development. By writing shared code that works across platforms, you can:

  • Reduce code duplication
  • Maintain consistent behavior
  • Speed up development
  • Simplify maintenance

We've covered the basics of writing common code, working with platform-specific requirements using expect/actual declarations, and creating interfaces to abstract platform functionality. We've also explored a real-world example of a networking client that can be used across platforms.

As you continue developing with Kotlin Multiplatform, focus on maximizing code sharing while respecting the unique requirements of each platform.

Additional Resources

Exercises

  1. Create a simple multiplatform project with a Person class in common code and platform-specific greeting functions
  2. Implement a DateFormatter class with expect/actual declarations to format dates according to platform conventions
  3. Build a simple calculator app with shared business logic in common code and platform-specific UIs
  4. Create a file storage utility with a common interface and platform-specific implementations
  5. Implement a simple API client using Ktor that can be used from both Android and iOS


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)