expect and actual: The Mechanism That Makes Kotlin Multiplatform Tick

The Core Problem KMP Has to Solve

Kotlin Multiplatform lets you share business logic across Android, iOS, desktop, and web — but each platform still has its own APIs. Android has Log, iOS has NSLog, Android has SharedPreferences, iOS has NSUserDefaults. KMP’s answer to this is a two-keyword mechanism: expect and actual. It’s the single most important concept to understand when starting with KMP, and it’s simpler than it sounds.

Circuit board representing platform layers
expect declares the contract in shared code; actual implements it per platform.

How It Works

In your shared commonMain module, you write an expect declaration — a function or class signature with no body. Think of it as an interface, but enforced by the compiler across source sets. Then, in each platform source set (androidMain, iosMain, etc.), you write the matching actual implementation using that platform’s native APIs.

The classic starter example is a platform logger:

// commonMain/Platform.kt
expect fun platformLog(tag: String, message: String)
// androidMain/Platform.kt
actual fun platformLog(tag: String, message: String) {
    android.util.Log.d(tag, message)
}
// iosMain/Platform.kt
actual fun platformLog(tag: String, message: String) {
    println("[$tag] $message")  // NSLog is also fine here
}

Your shared code calls platformLog() freely, and the compiler guarantees that every target platform has supplied an implementation. If you forget an actual, it’s a compile error — not a runtime surprise.

expect Classes and Interfaces

You’re not limited to top-level functions. You can use expect on classes, objects, and even annotations. A common real-world use is wrapping a platform-specific date/time source:

// commonMain
expect class Clock() {
    fun nowMillis(): Long
}

// Shared business logic uses Clock() freely:
class SessionManager(private val clock: Clock) {
    fun isExpired(expiryMillis: Long) = clock.nowMillis() > expiryMillis
}
// androidMain
actual class Clock actual constructor() {
    actual fun nowMillis(): Long = System.currentTimeMillis()
}
// iosMain
actual class Clock actual constructor() {
    actual fun nowMillis(): Long =
        (platform.Foundation.NSDate.date().timeIntervalSince1970 * 1000).toLong()
}

When to Use expect/actual vs Interfaces

A plain Kotlin interface works fine when you control all the implementations yourself and can pass them in via dependency injection. Reach for expect/actual when you need the compiler to enforce platform coverage, when the platform type itself needs to differ (not just the behaviour), or when you’re wrapping something that can’t be expressed as a constructor parameter — like a global singleton, an annotation, or a top-level function.

In practice, many KMP libraries use both: an interface for the public API that shared code depends on, and expect/actual to create the platform-specific factory that produces the correct implementation. It’s a clean separation of concerns that scales well as your shared module grows.

Summary

expect/actual is the bridge between your one shared codebase and the many platform worlds it lives in. Once you internalise that expect = the contract, actual = the delivery, the rest of KMP’s architecture falls into place naturally. Start with simple utility functions, then graduate to expect classes as your shared module takes on more responsibility.


This post was written by a human with the help of Claude, an AI assistant by Anthropic.

Scroll to Top