expect/actual is a Kotlin Multiplatform mechanism that allows declaring platform-dependent APIs in common code. The expect keyword creates a contract for a function, class or property in commonMain, while the actual keyword provides a concrete implementation for each platform. The compiler verifies that every expect declaration has a corresponding actual implementation on all target platforms. According to JetBrains, 2025, this mechanism is used in 80% of KMM projects for implementing platform business logic.
Key Takeaways
expect/actual is a declarative mechanism of Kotlin Multiplatform for implementing platform-oriented programming. It allows describing an API once in the common module (expect) and implementing it separately for each platform (actual). Unlike interfaces, expect/actual does not create virtual calls — the compiler links expect and actual declarations at compile time, eliminating the overhead of dynamic dispatch.
The history of expect/actual began with the introduction of Kotlin Multiplatform in 2017. Initially, the mechanism was called expect/actual declarations and was experimental. In Kotlin 1.2, expect annotations were added, and in Kotlin 1.3, expect/actual became stable for classes and functions. Over time the mechanism expanded: Kotlin 1.6 added support for expect/actual for companion objects, Kotlin 1.7 for enum classes, and Kotlin 2.0 for typealias.
The key feature of expect/actual is compile-time safety. If a developer adds an expect declaration in commonMain but forgets to provide an actual implementation for iOS, the compiler will produce an error. This prevents runtime failures common in approaches using reflection or dynamic loading of platform code.
The mechanism of expect/actual works at the source set level — the Kotlin Multiplatform module system. Common code available to all platforms resides in the commonMain source set. Platform-dependent code resides in iosMain, androidMain, macosMain, and so on. The expect keyword in commonMain declares an API, while the actual keyword in a platform source set provides the implementation. The compiler links them at the code generation stage, replacing the expect function call with the corresponding actual implementation for the target platform.
The source set hierarchy in a typical KMM project looks as follows: commonMain contains expect declarations, iosMain and androidMain contain actual implementations. When compiling for iOS, the actual from iosMain is used; when compiling for Android, the actual from androidMain is used. Source sets can be intermediate (e.g., iosArm64Main for a specific architecture), allowing implementations to be refined for different devices.
// commonMain — expect declaration
expect fun getPlatformName(): String
// androidMain — actual for Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual for iOS
actual fun getPlatformName(): String = "iOS"
The Kotlin compiler checks several conditions when working with expect/actual. Each expect declaration must have an actual implementation for each active platform. The signature of the actual declaration must match the expect signature (the @OptionalExpectation annotation can relax this requirement). Access modifiers, return type and parameters must be identical. The compiler also checks for cyclic dependencies between expect and actual declarations.
expect/actual supports several types of declarations. The most commonly used are expect/actual functions for platform operations, expect/actual classes for objects requiring native implementation, and expect/actual properties for constants and settings. Each type has its own usage rules and limitations.
Expect/actual functions are the simplest and most common type. They are used to call platform APIs such as getting the time, reading files, or sending HTTP requests. Expect/actual classes are used to create objects that directly interact with native code (e.g., for accessing the camera, geolocation, or key storage). Expect/actual properties (val) are suitable for platform constants — the OS name, SDK version, or system directory path.
| Declaration Type | Keywords | Usage Example |
|---|---|---|
| Function | expect fun / actual fun | Getting a unique device identifier |
| Class | expect class / actual class | Accessing SecureStorage (Keychain / EncryptedSharedPreferences) |
| Property | expect val / actual val | Current platform (iOS / Android) |
| Enum class | expect enum / actual enum | List of available app permissions |
| Typealias | expect typealias / actual typealias | Network response type specific to a platform |
Not all Kotlin constructs can be used with expect/actual. An expect declaration cannot contain a body — only a signature. An expect class cannot have a constructor with parameters (it must have an empty primary constructor). For enum expect/actual, all constants must be identical in both expect and actual. Expect properties must be val (not var), since storing state in the common module for platform properties does not make sense.
Let us explore practical examples of expect/actual from simple functions to full-fledged classes. The basic case is getting the platform name for use in the UI. More complex examples include accessing native storage and working with platform threads.
// commonMain — expect class for secure storage
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual on Android
actual class PlatformStorage {
private val prefs = AppContext.getSharedPreferences("secure", 0)
actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
actual fun get(key: String): String? = prefs.getString(key, null)
actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}
In this example, the expect class PlatformStorage defines the contract for a simple key-value storage. On Android, the implementation uses SharedPreferences, while on iOS it uses Keychain or NSUserDefaults. Thanks to expect/actual, the business logic in commonMain calls save/get/remove without knowing about the platform implementation.
// iosMain — actual on iOS with Keychain
actual class PlatformStorage {
actual fun save(key: String, value: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecValueData to value.encodeToByteArray()
)
SecItemAdd(query, null)
}
actual fun get(key: String): String? {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecReturnData to true
)
val result = mutableMapOf<String, Any>()
return if (SecItemCopyMatching(query, result) == errSecSuccess)
result[kSecValueData]?.toString()
else null
}
actual fun remove(key: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key
)
SecItemDelete(query)
}
}
When designing expect/actual APIs, several principles should be followed. Minimize the number of expect declarations — the more common code, the simpler the maintenance. Use expect/actual only for APIs that truly differ across platforms. For the rest of the code, use interfaces with factories or dependency injection, which simplifies testing.
It is recommended to group expect declarations by thematic modules, rather than mixing them in a single file. For example, Storage.kt for expect declarations related to storage, Platform.kt for expect functions working with the OS, and Analytics.kt for expect analytics classes. This simplifies navigation and understanding of the platform surface of a KMM project. Each actual file should reside in the corresponding source set: androidMain, iosMain, desktopMain, and so on.
Default implementations via expect fun with actual fun where actual uses common code is a common anti-pattern. If the platform implementation does not differ from the default, expect/actual is not needed. In such cases, use a simple function in commonMain. Also avoid expect/actual for trivial getters — use expect val with constants.
Proper structure of expect/actual code is critical for project readability. Each expect/actual module should have a single entry point. Example organization: commonMain/kotlin/com/project/platform contains expect declarations, androidMain/kotlin/com/project/platform contains actual for Android, iosMain/kotlin/com/project/platform contains actual for iOS. File and package names must match for expect and actual, so that a developer can quickly find the corresponding implementation.
Interfaces with a platform factory are the main alternative to expect/actual. Instead of an expect class, you can declare an interface in commonMain, and create concrete classes in platform modules. A factory or dependency injection container provides the correct implementation at runtime. This approach is better suited for testing since the interface can be mocked.
Dependency Injection (Koin, Kodein) is a more flexible but less performant approach. A DI container is configured separately for each platform and provides platform dependencies to common code. Unlike expect/actual, injection occurs at runtime, which allows swapping implementations for testing. On the other hand, DI configuration errors are only detected at runtime, not at compile time.
| Approach | Compile-time check | Testing flexibility | Runtime overhead |
|---|---|---|---|
| expect/actual | Full | Low (actual cannot be mocked) | Zero (compile-time binding) |
| Interfaces + Factory | Partial | High (can be mocked) | Minimal (virtual call) |
| Dependency Injection | No (runtime) | High | Moderate (DI proxies) |
The choice between expect/actual and alternatives depends on the context. For performance-critical code (game engines, real-time processing), expect/actual is preferable due to zero overhead. For business logic (repositories, use cases), it is better to use interfaces with DI to simplify testing. A combined approach — expect/actual for low-level platform operations and interfaces for the business logic layer — is used in most production KMM projects.
Frequently Asked Questions
expect/actual binds the implementation at compile time without virtual calls, while interfaces bind at runtime. expect/actual guarantees implementation for all platforms, while interfaces require runtime checks.
Yes, expect enum is supported starting from Kotlin 1.7. All constants in expect and actual enums must match. Different constant values on different platforms is a compilation error.
The compiler will produce an error for each platform where the actual implementation is missing. The project will not build until corresponding actual implementations are added for all expect declarations.
No, expect and actual must be in different source sets. expect in commonMain or an intermediate source set, actual in a platform source set. Placing expect and actual in the same source set is a compilation error.
To test expect/actual, use commonTest with platform test source sets. Write expect tests in commonTest and actual tests for each platform. Integration tests are run separately on each target platform.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also