How does expect/actual work in a KMP module, and where do you place each declaration across source sets?
answer
- expect in commonMain (no body)
- actual in each platform set (body)
- missing actual = compile error
- actual typealias reuses platform type
- place actual in most-specific covering source set
basics
~20 sYou write an expect declaration in commonMain describing an API with no body, then provide a matching actual implementation in each platform source set. The compiler links them so common code can call the platform-specific implementation.
solid answer
~50 s`expect` declares a function, property, class, or object in `commonMain` with the signature but **no implementation**. Each target source set that participates must provide a matching `actual` declaration that supplies the platform-specific body. The compiler checks that every `expect` has an `actual` for every target it compiles to — a missing `actual` is a compile error. Matching is by signature/name. Modern Kotlin also supports **`actual typealias`** (mapping an `expect class` onto an existing platform type, e.g. `actual typealias UUID = java.util.UUID` on JVM) and allows defaults: an `expect fun` can have a default in common that some targets override. Place `expect` in `commonMain`; place each `actual` in the most-specific source set that covers the needed targets — often a single platform set, but an intermediate set (e.g. `appleMain`) can hold one `actual` shared by several Apple targets.
code
kotlin · 12 lines// commonMain
expect class Platform() {
val name: String
}
// jvmMain
actual class Platform actual constructor() {
actual val name: String = "JVM"
}
// jvmMain alternative via typealias
// actual typealias UUID = java.util.UUIDgo deeper
Knows expect goes in common with no body and actual provides the platform implementation.
Explains compiler enforcement across all targets, actual typealias, and placing actuals in intermediate sets.
Chooses expect/actual vs. interface-injection deliberately and minimizes the expect surface for maintainability.
Sets module-wide conventions for when platform abstraction uses expect/actual vs. DI, weighing binary-compatibility and API evolution.
## The expect/actual mechanism KMP lets you call platform-specific code from shared code through the **`expect`/`actual`** keyword pair: - **`expect`** — declared in `commonMain`. It states *what* exists (signature, name, type) but has **no body**. It's a contract the common code can compile against. - **`actual`** — declared in a platform source set. It provides *how* the contract is implemented for that platform. ```kotlin // commonMain expect fun platformName(): String // jvmMain actual fun platformName(): String = "JVM ${System.getProperty("java.version")}" // iosArm64Main (or a shared appleMain) actual fun platformName(): String = "iOS" ``` ## What can be expect/actual Functions, top-level **properties**, **classes**, **interfaces**, **objects**, and **annotations**. For a class, every member declared in the `expect class` must be matched by the `actual class`. ## actual typealias Instead of writing a fresh implementation, an `expect class` can be satisfied by aliasing an existing platform type: ```kotlin // commonMain expect class AtomicRef<T>(initial: T) { fun get(): T } // jvmMain actual typealias AtomicRef<T> = java.util.concurrent.atomic.AtomicReference<T> ``` The platform type must already provide the expected members. ## Where to place actuals Put the `expect` in `commonMain`. Put each `actual` in the **most-specific source set** that covers the targets needing the same implementation: - One platform → that platform's set (`jvmMain`). - Several related platforms with shared code → an **intermediate** set such as `appleMain` or `nativeMain`, so one `actual` serves them all. ## Compiler enforcement The compiler requires **every** declared target reachable from the `expect` to have a matching `actual`. Add a new target, and any unmatched `expect` becomes a build error until you supply the `actual`. Signatures (parameter types, return type, name) must match exactly. ## When NOT to use expect/actual Expect/actual is the heaviest tool. For simple cases prefer an ordinary interface in common with platform implementations injected, or just a regular function passed in — reserve `expect/actual` for declarations that genuinely must resolve to a platform type at compile time.
- What happens if you add iosArm64() but forget its actual?The build fails: every expect must have a matching actual for each target it compiles to, so the missing iOS actual is a compile error.
- When is an interface in common better than expect/actual?When you don't need compile-time mapping to a platform type — an interface plus injected platform implementations is simpler and more testable.
expect is a job description posted at HQ; each office must hire an actual person (actual) who fits it — leave one office unstaffed and the build refuses to ship.
saying these in an interview costs you the question
- Putting expect in a platform source set instead of commonMain
- Thinking a missing actual is only a runtime error
- Not knowing actual typealias exists
- Reaching for expect/actual when a plain interface would do