Skip to main content

Kotlin Platform Specific Code

When developing multiplatform applications with Kotlin, you'll often encounter situations where you need to implement platform-specific functionality. Kotlin provides elegant mechanisms to handle these cases while maximizing code sharing. In this guide, we'll explore how to write and organize platform-specific code in Kotlin multiplatform projects.

Introduction to Platform-Specific Code​

Kotlin Multiplatform allows you to share code between different platforms (JVM, JS, Native). However, certain functionality might need different implementations depending on the target platform. For example:

  • File system operations
  • Network requests
  • UI components
  • Hardware access (camera, sensors)
  • Date formatting according to platform conventions

Kotlin provides several approaches to handle platform-specific code:

  1. Expect/Actual declarations
  2. Platform-specific extensions
  3. Conditional compilation with @OptIn
  4. Source set hierarchies

Let's explore each of these approaches in detail.

The Expect/Actual Mechanism​

The expect/actual mechanism is Kotlin's primary way to define platform-specific implementations of common code. You declare an API in common code with the expect keyword and provide platform-specific implementations with the actual keyword.

Basic Syntax​

In your common code:

kotlin
// Common code
expect class PlatformLogger() {
fun log(message: String)
}

In your platform-specific code:

kotlin
// JVM implementation
actual class PlatformLogger {
actual fun log(message: String) {
println("[JVM] $message")
}
}

// JS implementation
actual class PlatformLogger {
actual fun log(message: String) {
console.log("[JS] $message")
}
}

Complete Example: Platform-Specific Date Formatting​

Let's create a more practical example that formats dates according to platform conventions:

kotlin
// In commonMain
expect object DateFormatter {
fun formatDate(timeMillis: Long): String
}

// Usage in common code
fun displayDate(timeMillis: Long) {
println("The formatted date is: ${DateFormatter.formatDate(timeMillis)}")
}
kotlin
// In jvmMain
import java.text.SimpleDateFormat
import java.util.Date

actual object DateFormatter {
actual fun formatDate(timeMillis: Long): String {
val date = Date(timeMillis)
return SimpleDateFormat("MM/dd/yyyy").format(date)
}
}
kotlin
// In jsMain
actual object DateFormatter {
actual fun formatDate(timeMillis: Long): String {
val date = js("new Date(timeMillis)")
return js("date.toLocaleDateString()")
}
}

When running on the JVM, you might see output like:

The formatted date is: 08/15/2023

On JavaScript, you might see:

The formatted date is: 8/15/2023

Rules for Expect/Actual Declarations​

  1. Every expect declaration must have a corresponding actual declaration in each platform-specific source set.
  2. The actual declaration must match the expect declaration's signature (parameters, return types).
  3. You can use expect/actual for:
    • Functions
    • Properties
    • Classes
    • Objects
    • Interfaces

Platform-Specific Extensions​

Another approach is to create extension functions or properties that are only available on specific platforms:

kotlin
// In jvmMain
fun String.toJavaFile(): java.io.File {
return java.io.File(this)
}

// In jsMain
fun String.toJsDate(): dynamic {
return js("new Date(this)")
}

This approach doesn't require the expect/actual pattern but means the extensions will only be available in platform-specific code.

Conditional Compilation​

Kotlin provides experimental support for conditional compilation using the @OptIn annotation:

kotlin
import kotlin.experimental.ExperimentalStdlibApi

@OptIn(ExperimentalStdlibApi::class)
fun getPlatformName(): String {
return when {
Platform.isJvm -> "JVM"
Platform.isJs -> "JavaScript"
Platform.isNative -> "Native"
else -> "Unknown"
}
}

This approach is useful for small platform-specific adjustments within otherwise common code but should be used sparingly.

Source Set Hierarchies​

Kotlin Multiplatform projects use source sets to organize code. The standard structure includes:

  • commonMain - Common code for all platforms
  • jvmMain - JVM-specific code
  • jsMain - JavaScript-specific code
  • nativeMain - Native platforms code (which can be further specialized)

You can leverage this structure to organize your platform-specific implementations.

Example Project Structure​

src
├── commonMain
│ └── kotlin
│ └── com.example.app
│ └── CommonCode.kt
├── jvmMain
│ └── kotlin
│ └── com.example.app
│ └── JvmImplementations.kt
├── jsMain
│ └── kotlin
│ └── com.example.app
│ └── JsImplementations.kt
└── nativeMain
└── kotlin
└── com.example.app
└── NativeImplementations.kt

Real-World Example: HTTP Client​

Let's develop a more substantial example: a cross-platform HTTP client that works on different platforms:

kotlin
// In commonMain
expect class HttpClient() {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}

// Usage in common code
suspend fun fetchUserData(userId: String): UserData {
val client = HttpClient()
val response = client.get("https://api.example.com/users/$userId")
return parseUserData(response)
}
kotlin
// In jvmMain
import java.net.HttpURLConnection
import java.net.URL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

actual class HttpClient {
actual suspend fun get(url: String): String = withContext(Dispatchers.IO) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.inputStream.bufferedReader().use { it.readText() }
}

actual suspend fun post(url: String, body: String): String = withContext(Dispatchers.IO) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true

connection.outputStream.use { os ->
os.write(body.toByteArray())
}

connection.inputStream.bufferedReader().use { it.readText() }
}
}
kotlin
// In jsMain
import kotlinx.coroutines.await

actual class HttpClient {
actual suspend fun get(url: String): String {
val response = js("fetch(url)").await()
return response.text().await()
}

actual suspend fun post(url: String, body: String): String {
val options = js("{}")
options.method = "POST"
options.body = body

val response = js("fetch(url, options)").await()
return response.text().await()
}
}

This example demonstrates how you can create a unified API in common code while providing platform-specific implementations using technologies native to each platform.

Best Practices for Platform-Specific Code​

  1. Keep the common API minimal: Design your expect declarations to expose only what's necessary, hiding platform-specific complexities.

  2. Use interfaces for complex behavior: For complex behavior, define interfaces in common code and implement them in platform-specific code.

  3. Isolate platform-specific code: Keep platform-specific code isolated to make maintenance easier.

  4. Use composition over inheritance: When designing multiplatform components, favor composition over inheritance when possible.

  5. Test on all target platforms: Make sure to test your implementations on all target platforms to ensure consistent behavior.

When to Use Platform-Specific Code​

You should consider platform-specific implementations when:

  1. There's no common API available
  2. Performance is critical and platform-specific optimizations are necessary
  3. You need to integrate with platform-specific libraries
  4. Different platforms have significantly different ways of handling a problem

Summary​

Kotlin's approach to platform-specific code through the expect/actual mechanism and source set organization provides powerful tools for building multiplatform applications. It allows you to:

  • Share as much code as possible
  • Provide custom implementations where necessary
  • Create unified APIs that work across platforms
  • Leverage platform-specific features when needed

By understanding and applying these mechanisms effectively, you can develop multiplatform applications that are both highly maintainable and optimized for each target platform.

Additional Resources​

Exercises​

  1. Create a simple file logger that works on JVM and Native platforms using expect/actual.
  2. Implement a platform-specific device information class that returns device name, OS version, and available memory.
  3. Build a simple settings storage system that uses SharedPreferences on Android, UserDefaults on iOS, and LocalStorage on JS.
  4. Create platform-specific date/time pickers that integrate with the native UI frameworks but provide a common API.


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)