Kotlin Multiplatform Development Help

Use platform-specific APIs

In this article, you'll learn how to use platform-specific APIs when developing multiplatform applications and libraries.

Kotlin multiplatform libraries

Before writing code that uses a platform-specific API, check whether you can use a multiplatform library instead. This type of library provides a common Kotlin API that has a different implementation for different platforms.

There are already many libraries available that you can use to implement networking, logging, and analytics, as well as access device functionality and more. For more information, see this curated list.

Expected and actual functions and properties

Kotlin provides a language mechanism to access platform-specific APIs while developing common logic: expected and actual declarations.

With this mechanism, the common source set of a multiplatform module defines an expected declaration, and every platform source set must provide the actual declaration that corresponds to the expected declaration. The compiler ensures that every declaration marked with the expect keyword in the common source set has the corresponding declarations marked with the actual keyword in all targeted platform source sets.

This works for most Kotlin declarations, such as functions, classes, interfaces, enumerations, properties, and annotations. This section focuses on using expected and actual functions and properties.

Using expected and actual functions and properties

In this example, you'll define an expected platform() function in the common source set and provide the actual implementations in the platform source sets. While generating the code for a specific platform, the Kotlin compiler merges the expected and actual declarations. It generates one platform() function with its actual implementation. The expected and actual declarations should be defined in the same package and merged into one declaration in the resulting platform code. Any invocation of the expected platform() function in the generated platform code will call the correct actual implementation.

Example: generate a UUID

Let's assume that you are developing iOS and Android applications using Kotlin Multiplatform and you want to generate a universally unique identifier (UUID).

To do so, declare the expected function randomUUID() with the expect keyword in the common source set of your Kotlin Multiplatform module. Do not include any implementation code.

// In the common source set: expect fun randomUUID(): String

In each platform-specific source set (iOS and Android), provide the actual implementation for the randomUUID() function expected in the common module. Use the actual keyword to mark these actual implementations.

Generating UUID with expected and actual declarations

The following snippets show the implementations for Android and iOS. Platform-specific code uses the actual keyword and the same name for the function:

// In the android source set: import java.util.* actual fun randomUUID() = UUID.randomUUID().toString()
// In the iOS source set: import platform.Foundation.NSUUID actual fun randomUUID(): String = NSUUID().UUIDString()

The Android implementation uses the APIs available on Android, while the iOS implementation uses the APIs available on iOS. You can access iOS APIs from Kotlin/Native code.

While producing the resulting platform code for Android, the Kotlin compiler automatically merges the expected and actual declarations and generates a single randomUUID() function with its actual Android-specific implementation. The same process is repeated for iOS.

For simplicity, this and the following examples use the simplified source set names "common", "ios", and "android". Typically, this implies commonMain, iosMain, and androidMain, and similar logic can be defined in the test source sets commonTest, iosTest, and androidTest.

Similar to expected and actual functions, expected and actual properties allow you to use different values on different platforms. Expected and actual functions and properties are most useful for simple cases.

Interfaces in common code

If the platform-specific logic is too big and complex, you can simplify your code by defining an interface to represent it in the common code and then providing different implementations in the platform source sets.

Using interfaces

The implementations in the platform source sets use their corresponding dependencies:

// In the commonMain source set: interface Platform { val name: String }
// In the androidMain source set: import android.os.Build class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" }
// In the iosMain source set: import platform.UIKit.UIDevice class IOSPlatform : Platform { override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion }

To inject the appropriate platform implementations when you need a common interface, you can choose one of the following options, each of which is explained in more detail below:

Expected and actual functions

Define an expected function that returns a value of this interface, and then define actual functions that return its subclasses:

// In the commonMain source set: interface Platform expect fun platform(): Platform
// In the androidMain source set: class AndroidPlatform : Platform actual fun platform() = AndroidPlatform()
// In the iosMain source set: class IOSPlatform : Platform actual fun platform() = IOSPlatform()

When you call the platform() function in the common code, it can work with an object of the Platform type. When you run this common code on Android, the platform() call returns an instance of the AndroidPlatform class. When you run it on iOS, platform() returns an instance of the IOSPlatform class.

Different entry points

If you control the entry points, you can construct implementations of each platform artifact without using expected and actual declarations. To do so, define the platform implementations in the shared Kotlin Multiplatform module, but instantiate them in the platform modules:

// Shared Kotlin Multiplatform module // In the commonMain source set: interface Platform fun application(p: Platform) { // application logic }
// In the androidMain source set: class AndroidPlatform : Platform
// In the iosMain source set: class IOSPlatform : Platform
// In the androidApp platform module: import android.app.Application import mysharedpackage.* class MyApp : Application() { override fun onCreate() { super.onCreate() application(AndroidPlatform()) } }
// In the iosApp platform module (in Swift): import shared @main struct iOSApp : App { init() { application(IOSPlatform()) } }

On Android, you should create an instance of AndroidPlatform and pass it to the application() function, while on iOS, you should similarly create and pass an instance of IOSPlatform. These entry points don't need to be the entry points of your applications, but this is where you can call the specific functionality of your shared module.

Providing the right implementations with expected and actual functions or directly through entry points works well for simple scenarios. However, if you use a dependency injection framework in your project, we recommend using it in simple cases to ensure consistency.

Dependency injection framework

A modern application typically uses a dependency injection (DI) framework to create a loosely coupled architecture. The DI framework allows injecting dependencies into components based on the current environment.

Any DI framework that supports Kotlin Multiplatform can help you inject different dependencies for different platforms.

For example, Koin is a dependency injection framework that supports Kotlin Multiplatform:

// In the common source set: import org.koin.dsl.module interface Platform expect val platformModule: Module
// In the androidMain source set: class AndroidPlatform : Platform actual val platformModule: Module = module { single<Platform> { AndroidPlatform() } }
// In the iosMain source set: class IOSPlatform : Platform actual val platformModule = module { single<Platform> { IOSPlatform() } }

Here, Koin DSLs create modules that define components for injection. You declare a module in common code with the expect keyword and then provide a platform-specific implementation for each platform using the actual keyword. The framework takes care of selecting the correct implementation at runtime.

When you use a DI framework, you inject all of the dependencies through this framework. The same logic applies to handling platform dependencies. We recommend continuing to use DI if you already have it in your project, rather than using the expected and actual functions manually. This way, you can avoid mixing two different ways of injecting dependencies.

You also don't have to always implement the common interface in Kotlin. You can do it in another language, like Swift, in a different platform module. If you choose this approach, you should then provide the implementation from the iOS platform module using the DI framework:

Using dependency injection framework

This approach only works if you put the implementations in the platform modules. It isn't very scalable, as your Kotlin Multiplatform module can't be self-sufficient and you'll need to implement the common interface in a different module.

What's next?

For more examples and information on the expect/actual mechanism, see Expected and actual declarations.

Last modified: 18 December 2023