Expected and actual declarations
Expected and actual declarations allow you to access platform-specific APIs from Kotlin Multiplatform modules. You can provide platform-agnostic APIs in the common code.
Rules for expected and actual declarations
To define expected and actual declarations, follow these rules:
In the common source set, declare a standard Kotlin construct. This can be a function, property, class, interface, enumeration, or annotation.
Mark this construct with the
expect
keyword. This is your expected declaration. These declarations can be used in the common code, but shouldn't include any implementation. Instead, the platform-specific code provides this implementation.In each platform-specific source set, declare the same construct in the same package and mark it with the
actual
keyword. This is your actual declaration, which typically contains an implementation using platform-specific libraries.
During compilation for a specific target, the compiler tries to match each actual declaration it finds with the corresponding expected declaration in the common code. The compiler ensures that:
Every expected declaration in the common source set has a matching actual declaration in every platform-specific source set.
Expected declarations don't contain any implementation.
Every actual declaration shares the same package as the corresponding expected declaration, such as
org.mygroup.myapp.MyType
.
While generating the resulting code for different platforms, the Kotlin compiler merges the expected and actual declarations that correspond to each other. It generates one declaration with its actual implementation for each platform. Every use of the expected declaration in the common code calls the correct actual declaration in the resulting platform code.
You can declare actual declarations when you use intermediate source sets shared between different target platforms. Consider, for example, iosMain
as an intermediate source set shared between the iosX64Main
, iosArm64Main
, and iosSimulatorArm64Main
platform source sets. Only iosMain
typically contains the actual declarations and not the platform source sets. The Kotlin compiler will then use these actual declarations to produce the resulting code for the corresponding platforms.
The IDE assists with common issues, including:
Missing declarations
Expected declarations that contain implementations
Mismatched declaration signatures
Declarations in different packages
You can also use the IDE to navigate from expected to actual declarations. Select the gutter icon to view actual declarations or use shortcuts.
Different approaches for using expected and actual declarations
Let's explore the different options of using the expect/actual mechanism to solve the problem of accessing platform APIs while still providing a way to work with them in the common code.
Consider a Kotlin Multiplatform project where you need to implement the Identity
type, which should contain the user's login name and the current process ID. The project has the commonMain
, jvmMain
, and nativeMain
source sets to make the application work on the JVM and in native environments like iOS.
Expected and actual functions
You can define an Identity
type and a factory function buildIdentity()
, which is declared in the common source set and implemented differently in platform source sets:
In
commonMain
, declare a simple type and expect a factory function:package identity class Identity(val userName: String, val processID: Long) expect fun buildIdentity(): IdentityIn the
jvmMain
source set, implement a solution using standard Java libraries:package identity import java.lang.System import java.lang.ProcessHandle actual fun buildIdentity() = Identity( System.getProperty("user.name") ?: "None", ProcessHandle.current().pid() )In the
nativeMain
source set, implement a solution with POSIX using native dependencies:package identity import kotlinx.cinterop.toKString import platform.posix.getlogin import platform.posix.getpid actual fun buildIdentity() = Identity( getlogin()?.toKString() ?: "None", getpid().toLong() )
Here, platform functions return platform-specific Identity
instances.
Interfaces with expected and actual functions
If the factory function becomes too large, consider using a common Identity
interface and implementing it differently on different platforms.
A buildIdentity()
factory function should return Identity
, but this time, it's an object implementing the common interface:
In
commonMain
, define theIdentity
interface and thebuildIdentity()
factory function:// In the commonMain source set: expect fun buildIdentity(): Identity interface Identity { val userName: String val processID: Long }Create platform-specific implementations of the interface without additional use of expected and actual declarations:
// In the jvmMain source set: actual fun buildIdentity(): Identity = JVMIdentity() class JVMIdentity( override val userName: String = System.getProperty("user.name") ?: "none", override val processID: Long = ProcessHandle.current().pid() ) : Identity// In the nativeMain source set: actual fun buildIdentity(): Identity = NativeIdentity() class NativeIdentity( override val userName: String = getlogin()?.toKString() ?: "None", override val processID: Long = getpid().toLong() ) : Identity
These platform functions return platform-specific Identity
instances, which are implemented as JVMIdentity
and NativeIdentity
platform types.
Expected and actual properties
You can modify the previous example and expect a val
property to store an Identity
.
Mark this property as expect val
and then actualize it in the platform source sets:
Expected and actual objects
When IdentityBuilder
is expected to be a singleton on each platform, you can define it as an expected object and let the platforms actualize it:
Recommendations on dependency injection
To create a loosely coupled architecture, many Kotlin projects adopt the dependency injection (DI) framework. The DI framework allows injecting dependencies into components based on the current environment.
For example, you might inject different dependencies in testing and in production or when deploying to the cloud compared to hosting locally. As long as a dependency is expressed through an interface, any number of different implementations can be injected, either at compile time or at runtime.
The same principle applies when the dependencies are platform-specific. In the common code, a component can express its dependencies using regular Kotlin interfaces. The DI framework can then be configured to inject a platform-specific implementation, for example, from the JVM or an iOS module.
This means that expected and actual declarations are only needed in the configuration of the DI framework. See Use platform-specific APIs for examples.
With this approach, you can adopt Kotlin Multiplatform simply by using interfaces and factory functions. If you already use the DI framework to manage dependencies in your project, we recommend using the same approach for managing platform dependencies.
Expected and actual classes
You can use expected and actual classes to implement the same solution:
You might have already seen this approach in demonstration materials. However, using classes in simple cases where interfaces would be sufficient is not recommended.
With interfaces, you don't limit your design to one implementation per target platform. Also, it's much easier to substitute a fake implementation in tests or provide multiple implementations on a single platform.
As a general rule, rely on standard language constructs wherever possible instead of using expected and actual declarations.
If you do decide to use expected and actual classes, the Kotlin compiler will warn you about the Beta status of the feature. To suppress this warning, add the following compiler option to your Gradle build file:
Inheritance from platform classes
There are special cases when using the expect
keyword with classes may be the best approach. Let's say that the Identity
type already exists on the JVM:
To fit it in the existing codebase and frameworks, your implementation of the Identity
type can inherit from this type and reuse its functionality:
To solve this problem, declare a class in
commonMain
using theexpect
keyword:expect class CommonIdentity() { val userName: String val processID: Long }In
nativeMain
, provide an actual declaration that implements the functionality:actual class CommonIdentity { actual val userName = getlogin()?.toKString() ?: "None" actual val processID = getpid().toLong() }In
jvmMain
, provide an actual declaration that inherits from the platform-specific base class:actual class CommonIdentity : Identity() { actual val userName = login actual val processID = pid }
Here, the CommonIdentity
type is compatible with your own design while taking advantage of the existing type on the JVM.
Application in frameworks
As a framework author, you can also find expected and actual declarations useful for your framework.
If the example above is part of a framework, the user has to derive a type from CommonIdentity
to provide a display name.
In this case, the expected declaration is abstract and declares an abstract method:
Similarly, actual implementations are abstract and declare the displayName
method:
The framework users need to write common code that inherits from the expected declaration and implement the missing method themselves:
Advanced use cases
There are a number of special cases regarding expected and actual declarations.
Using type aliases to satisfy actual declarations
The implementation of an actual declaration does not have to be written from scratch. It can be an existing type, such as a class provided by a third-party library.
You can use this type as long as it meets all the requirements associated with the expected declaration. For example, consider these two expected declarations:
Within a JVM module, the java.time.Month
enum can be used to implement the first expected declaration and the java.time.LocalDate
class to implement the second. However, there's no way to add the actual
keyword directly to these types.
Instead, you can use type aliases to connect the expected declarations and the platform-specific types:
In this case, define the typealias
declaration in the same package as the expected declaration and create the referred class elsewhere.
Expanded visibility in actual declarations
You can make actual implementations more visible than the corresponding expected declaration. This is useful if you don't want to expose your API as public for common clients.
Currently, the Kotlin compiler issues an error in the case of visibility changes. You can suppress this error by applying @Suppress("ACTUAL_WITHOUT_EXPECT")
to the actual type alias declaration. Starting with Kotlin 2.0, this limitation will not apply.
For example, if you declare the following expected declaration in the common source set:
You can use the following actual implementation in a platform-specific source set as well:
Here, an internal expected class has an actual implementation with an existing public MyMessenger
using type aliases.
Additional enumeration entries on actualization
When an enumeration is declared with expect
in the common source set, each platform module should have a corresponding actual
declaration. These declarations must contain the same enum constants, but they can also have additional constants.
This is useful when you actualize an expected enum with an existing platform enum. For example, consider the following enumeration in the common source set:
When you provide an actual declaration for Department
in platform source sets, you can add extra constants:
However, in this case, these extra constants in the platform source sets won't match with those in the common code. Therefore, the compiler requires you to handle all additional cases.
The function that implements the when
construction on Department
requires an else
clause:
Expected annotation classes
Expected and actual declarations can be used with annotations. For example, you can declare an @XmlSerializable
annotation, which must have a corresponding actual declaration in each platform source set:
It might be helpful to reuse existing types on a particular platform. For example, on the JVM, you can define your annotation using the existing type from the JAXB specification:
There is an additional consideration when using expect
with annotation classes. Annotations are used to attach metadata to code and do not appear as types in signatures. It's not essential for an expected annotation to have an actual class on a platform where it's never required.
You only need to provide an actual
declaration on platforms where the annotation is used. This behavior isn't enabled by default, and it requires the type to be marked with OptionalExpectation
.
Take the @XmlSerializable
annotation declared above and add OptionalExpectation
:
If an actual declaration is missing on a platform where it's not required, the compiler won't generate an error.
What's next?
For general recommendations on different ways to use platform-specific APIs, see Use platform-specific APIs.