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:
- Expect/Actual declarations
- Platform-specific extensions
- Conditional compilation with @OptIn
- 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:
// Common code
expect class PlatformLogger() {
    fun log(message: String)
}
In your platform-specific code:
// 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:
// 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)}")
}
// 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)
    }
}
// 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
- Every expectdeclaration must have a correspondingactualdeclaration in each platform-specific source set.
- The actualdeclaration must match theexpectdeclaration's signature (parameters, return types).
- You can use expect/actualfor:- Functions
- Properties
- Classes
- Objects
- Interfaces
 
Platform-Specific Extensions
Another approach is to create extension functions or properties that are only available on specific platforms:
// 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:
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:
// 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)
}
// 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() }
    }
}
// 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
- 
Keep the common API minimal: Design your expect declarations to expose only what's necessary, hiding platform-specific complexities. 
- 
Use interfaces for complex behavior: For complex behavior, define interfaces in common code and implement them in platform-specific code. 
- 
Isolate platform-specific code: Keep platform-specific code isolated to make maintenance easier. 
- 
Use composition over inheritance: When designing multiplatform components, favor composition over inheritance when possible. 
- 
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:
- There's no common API available
- Performance is critical and platform-specific optimizations are necessary
- You need to integrate with platform-specific libraries
- 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
- Kotlin Multiplatform Official Documentation
- Kotlin Expect/Actual Mechanism Documentation
- KotlinX Libraries for Multiplatform Development
Exercises
- Create a simple file logger that works on JVM and Native platforms using expect/actual.
- Implement a platform-specific device information class that returns device name, OS version, and available memory.
- Build a simple settings storage system that uses SharedPreferences on Android, UserDefaults on iOS, and LocalStorage on JS.
- Create platform-specific date/time pickers that integrate with the native UI frameworks but provide a common API.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!