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 [email protected]. I’d greatly appreciate your feedback! :)