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.
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 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.
The following snippets show the implementations for Android and iOS. Platform-specific code uses the actual
keyword and the same name for the function:
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.
The implementations in the platform source sets use their corresponding dependencies:
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:
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:
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:
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:
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.