# expect/actual or interface: platform-specific code in Kotlin Multiplatform

> When to use expect/actual and when a plain interface with an injected implementation is enough. Android and iOS code, a unit test, and the Beta status of expect classes on Kotlin 2.4.

- URL: https://dantech.academy/en/blog/kotlin/expect-actual-hay-interface-kotlin-multiplatform
- Published: 2026-11-05
- Reading time: 5 min
- Tags: kotlin, multiplatform, android, architecture

## The problem

In a Kotlin Multiplatform (KMP) app, most of the logic lives in `commonMain`: ViewModels, repositories, game rules, data formatting. Sooner or later you need something only the platform has, such as the system clock, the clipboard, or an Android `Context`.

Kotlin gives you two ways to reach those from shared code:

1. `expect`/`actual`: declare it in `commonMain`, and each platform writes its own `actual`.
2. A plain Kotlin `interface` in `commonMain`, a class per platform that implements it, and the implementation passed in from outside.

Both work. This post helps you pick the right one for each spot, based on the official Kotlin docs and code I built on Kotlin 2.4.

## How expect/actual works

You declare a function with `expect` in `commonMain`, with no body:

```kotlin
// commonMain/kotlin/com/example/match/Now.kt
package com.example.match

expect fun currentTimeMillis(): Long
```

Each platform source set writes the `actual`, in the same package:

```kotlin
// androidMain/kotlin/com/example/match/Now.android.kt
package com.example.match

actual fun currentTimeMillis(): Long = System.currentTimeMillis()
```

```kotlin
// iosMain/kotlin/com/example/match/Now.ios.kt
package com.example.match

import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970

actual fun currentTimeMillis(): Long = (NSDate().timeIntervalSince1970 * 1000).toLong()
```

When it builds each target, the compiler merges the `expect` with the matching `actual` into one declaration. It also checks three things for you: every `expect` has an `actual` on every platform you build, the `expect` side has no implementation, and both sides share a package. Forget the iOS `actual` and the iOS build fails right away.

This fits small things with no state: reading the clock, choosing a `CoroutineDispatcher` for IO, getting the platform name.

## Where expect fun gets awkward

Say you want to copy a match result to the clipboard. On Android that needs a `Context`. `commonMain` has no idea what a `Context` is, so you can't write `expect fun copy(context: Context, text: String)`.

You could keep a `Context` in a global variable in `androidMain`, but that is hidden state: tests get harder, and shared code depends on something it can't see.

An interface is much simpler here:

```kotlin
// commonMain/kotlin/com/example/match/Clipboard.kt
package com.example.match

interface Clipboard {
    fun copy(text: String)
}

class ShareResult(private val clipboard: Clipboard) {
    fun copyWinner(winner: String) {
        clipboard.copy("Winner: $winner")
    }
}
```

Each platform implements the interface with its own API. The Android version takes a `Context` in its constructor:

```kotlin
// androidMain/kotlin/com/example/match/AndroidClipboard.kt
package com.example.match

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context

class AndroidClipboard(private val context: Context) : Clipboard {
    override fun copy(text: String) {
        val manager = context.getSystemService(ClipboardManager::class.java)
        manager.setPrimaryClip(ClipData.newPlainText("match result", text))
    }
}
```

```kotlin
// iosMain/kotlin/com/example/match/IosClipboard.kt
package com.example.match

import platform.UIKit.UIPasteboard

class IosClipboard : Clipboard {
    override fun copy(text: String) {
        UIPasteboard.generalPasteboard.string = text
    }
}
```

The objects are created in platform code, where that information exists:

```kotlin
// Android, for example inside an Activity
val shareResult = ShareResult(AndroidClipboard(applicationContext))

// iOS
val shareResult = ShareResult(IosClipboard())
```

If the project already uses a DI framework such as Koin, register each platform's implementation in a DI module instead of those two lines. The Kotlin docs suggest the same thing: when a dependency is an interface, `expect`/`actual` is only needed in the DI configuration, if at all.

## Interfaces are easier to test

`commonTest` is compiled and run separately for each target. With an `expect fun`, a test always calls the real `actual` of the target it runs on, and you can't swap in a fake.

With an interface, you write the fake right in `commonTest`:

```kotlin
// commonTest/kotlin/com/example/match/ShareResultTest.kt
package com.example.match

import kotlin.test.Test
import kotlin.test.assertEquals

class FakeClipboard : Clipboard {
    val copied = mutableListOf<String>()

    override fun copy(text: String) {
        copied += text
    }
}

class ShareResultTest {
    @Test
    fun copiesTheWinnerLine() {
        val clipboard = FakeClipboard()
        ShareResult(clipboard).copyWinner("X")
        assertEquals(listOf("Winner: X"), clipboard.copied)
    }
}
```

This test needs no emulator, never touches the real clipboard, and behaves the same on every target.

## What about expect class?

You can also write `expect class`, `expect object` or `expect interface`. According to the current Kotlin docs, expected and actual classes are still in Beta. I built a sample on Kotlin 2.4.10 (the latest release at the time of writing is 2.4.21), and the compiler still prints this warning:

```text
'expect'/'actual' classes (including interfaces, objects, annotations, enums, and 'actual' typealiases) are in Beta. Consider using the '-Xexpect-actual-classes' flag to suppress this warning.
```

If you've thought it through and still want them, add this flag in `build.gradle.kts` to silence the warning:

```kotlin
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xexpect-actual-classes")
    }
}
```

The docs advise against `expect class` in simple cases an interface can handle, for the same reasons as above: an interface allows several implementations on one platform and is easy to replace with a fake in tests. An `expect class` earns its place in a few specific cases:

- The `actual` needs to inherit from an existing platform class.
- You want to point straight at an existing type with `actual typealias`, for example `actual typealias MyDate = java.time.LocalDate` on the JVM.
- You're writing a framework and need a shared base class for users to extend.

## Which one to use

| Situation | Use |
| --- | --- |
| A small function or value with no state that tests don't need to replace (clock, dispatcher, platform name) | `expect fun` / `expect val` |
| Needs a platform dependency such as `Context`, holds state, or needs a fake in tests | `interface` + pass the implementation in |
| The `actual` must inherit a platform class, or point at an existing type with `typealias` | `expect class` (accept Beta) |

When in doubt, start with an interface. The Kotlin docs say it plainly: rely on standard language constructs wherever possible, and use `expect`/`actual` only when you need them.

## Keep learning

In [Kotlin Mobile 101](https://dantech.academy/courses/kotlin-mobile-101) you read and change a complete Kotlin Multiplatform app. It uses `expect`/`actual` in some places and interfaces in others, with a unit test suite so you can check each change you make.
