skip to content

In Kotlin Multiplatform, what do the `expect` and `actual` keywords do, and why does a library use them to back one common API across targets?

level: juniorimportance: must knowfreq 70%

answer

  1. expect = declaration no body in commonMain
  2. actual = real impl per platform source set
  3. compile-time link, not reflection
  4. every target must supply an actual
  5. actual typealias reuses a native type

basics

~10 s

expect declares a shared API in common code with no body; each platform (Android, iOS, JS) provides an actual implementation. So you call one function everywhere and the right platform version runs.

solid answer

~40 s

Kotlin Multiplatform splits code into a `commonMain` source set plus platform source sets (e.g. `androidMain`, `iosMain`, `jsMain`). In `commonMain` you write `expect fun`/`expect class`/`expect val` — a declaration with no implementation. Each platform source set must supply a matching `actual` with the same signature, fully implemented. At compile time the compiler links the common `expect` to the platform's `actual`, so the final binary for that target contains a real implementation. Libraries use this to expose ONE common API (e.g. `Dispatchers.Main`, an HTTP client, a database driver) while delegating to platform-native primitives underneath — UI loopers on Android, NSRunLoop on iOS. Callers in common code stay platform-agnostic; only the library's internals branch. It is resolved statically, not via runtime reflection.

code

kotlin · 10 lines
kotlin
// commonMain
expect fun currentTimeMillis(): Long

// androidMain / jvmMain
actual fun currentTimeMillis(): Long = System.currentTimeMillis()

// iosMain
import platform.Foundation.NSDate
actual fun currentTimeMillis(): Long =
    (NSDate().timeIntervalSince1970 * 1000).toLong()

go deeper

for a junior

Knows expect declares, actual implements, one per platform; can give a simple example.

for a middle

Explains source sets, compile-time linking, and that every target must supply an actual.

for a senior

Adds actual typealias, intermediate source-set actuals via the hierarchy, and how real libraries apply it.

for a principal

Discusses API-design tradeoffs of expect/actual vs interface injection, exhaustiveness guarantees, and binary/ABI implications across targets.

## What problem this solves Kotlin Multiplatform (KMP) lets you share one body of Kotlin code across **targets** (JVM/Android, Apple/iOS via Kotlin/Native, JS, etc.). But some functionality has no portable implementation — getting the UI thread, opening a socket, reading a file. The `expect`/`actual` mechanism lets a library publish **one common API** and back it with a **different real implementation per target**. ## Source sets KMP code is organized into **source sets**: - `commonMain` — shared code compiled for every target. - platform sets like `androidMain`, `iosMain` (often `iosX64Main`/`iosArm64Main` merged via a hierarchy), `jsMain`, `jvmMain`. ## `expect` — the contract In `commonMain` you write a declaration with **no body**: ```kotlin // commonMain expect val platformName: String expect fun randomUUID(): String expect class Platform() { val name: String } ``` You can `expect` on `fun`, `val`, `class`, `object`, `interface`, even `annotation class` and `enum`. ## `actual` — the implementation Each target source set MUST provide a matching `actual` with the **same signature**: ```kotlin // androidMain actual val platformName: String = "Android ${android.os.Build.VERSION.SDK_INT}" actual fun randomUUID(): String = java.util.UUID.randomUUID().toString() actual class Platform actual constructor() { actual val name: String = "Android" } ``` ```kotlin // iosMain import platform.Foundation.NSUUID actual val platformName: String = "iOS" actual fun randomUUID(): String = NSUUID().UUIDString() ``` ## Key properties - **Compile-time linking, not reflection.** The compiler matches `expect` to `actual` when building each target; the resulting binary contains only that target's code. There is no runtime dispatch or service lookup. - **Exhaustive.** If any target lacks an `actual`, compilation of that target fails. Common code can never call an unimplemented API. - **`actual typealias`.** A platform can satisfy an `expect class` by aliasing an existing platform type: `actual typealias UUID = java.util.UUID`. This reuses the native class directly. - **Hierarchy & defaults.** With the default hierarchy template, intermediate sets (e.g. `appleMain`) can hold shared `actual`s for several Apple targets. Modern Kotlin also allows `expect` declarations with default bodies that a target may override. ## How libraries use it - **kotlinx.coroutines** exposes `Dispatchers.Main` as a common API; the JVM/Android `actual` uses the main `Looper`/`Handler`, the JS `actual` uses the microtask/event loop, Native uses the main run loop. - **Ktor** has a common `HttpClient`; engines (`OkHttp`, `Darwin`, `Js`, `CIO`) are the platform-specific backends selected per target. - **SQLDelight** exposes a common `SqlDriver`; `AndroidSqliteDriver`, `NativeSqliteDriver`, and a JS/Web worker driver are the actuals. The takeaway: `expect`/`actual` is the seam that keeps **call sites portable** while letting the implementation be **native per platform**.

  • What happens if one target is missing an `actual` for an `expect` declaration?
    Compilation of that specific target fails with an unresolved-expectation error; you cannot ship a binary without supplying every actual.
  • Can an `actual` be a typealias?
    Yes. `actual typealias Platform = SomeNativeType` satisfies an `expect class` by reusing an existing platform type, avoiding a hand-written wrapper.

Like an interface (expect) whose concrete class (actual) is chosen by which platform you compile for, decided at build time rather than runtime.

saying these in an interview costs you the question

  • Saying expect/actual is resolved at runtime via reflection or service loaders
  • Thinking you only need an actual on 'the main' platform
  • Confusing expect/actual with regular interface/implementation chosen at runtime
  • Believing common code can call an expect with no actuals supplied
  • Claiming actual signatures may differ from the expect

context