expect/actual — essence, KMM keywords and how they work

Author: IT Sectr Published: 2026-06-05 Reading time: 8 min

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 is the keyword for declaring a contract of a function, class or property in common code.
  • actual is the keyword for providing a platform-specific implementation of an expect declaration.
  • commonMain is the source set with common code where expect declarations are placed.
  • Compiler check — the compiler guarantees that actual implementations exist for all target platforms.
  • Source set — sets (iosMain, androidMain) where platform-specific actual implementations reside.

What is expect/actual?

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.

How the expect/actual mechanism works

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.

kotlin
// 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"

Compiler verification of actual implementations

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.

Types of expect/actual: functions, classes, properties

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 TypeKeywordsUsage Example
Functionexpect fun / actual funGetting a unique device identifier
Classexpect class / actual classAccessing SecureStorage (Keychain / EncryptedSharedPreferences)
Propertyexpect val / actual valCurrent platform (iOS / Android)
Enum classexpect enum / actual enumList of available app permissions
Typealiasexpect typealias / actual typealiasNetwork response type specific to a platform

Limitations of expect/actual

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.

Code examples: from simple to complex

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.

kotlin
// 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.

kotlin
// 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)
    }
}

Best practices for expect/actual

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.

Code organization in a project

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.

Alternatives to expect/actual in KMM

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.

ApproachCompile-time checkTesting flexibilityRuntime overhead
expect/actualFullLow (actual cannot be mocked)Zero (compile-time binding)
Interfaces + FactoryPartialHigh (can be mocked)Minimal (virtual call)
Dependency InjectionNo (runtime)HighModerate (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

What is the difference between expect/actual and interfaces?

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.

Can expect/actual be used for enums?

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.

What happens if an actual implementation is missing?

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.

Can expect/actual be used within a single source set?

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.

How to test expect/actual code?

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

  • expect/actual is the key Kotlin Multiplatform mechanism for platform implementations with compiler verification.
  • expect declares a contract in commonMain, actual provides the implementation in a platform source set.
  • Declaration types include functions, classes, properties, enum classes and typealias with different usage rules.
  • Compiler verification guarantees actual implementations for all target platforms, preventing runtime failures.
  • It is recommended to minimize expect/actual and use interfaces with DI for business logic.
  • Code organization should be consistent with matching file and package names for expect and actual.
  • Use expect/actual for low-level platform operations (storage, file system, sensors) — this ensures zero runtime overhead.

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.

Discuss the project

Read also