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
expect
declaration must have a correspondingactual
declaration in each platform-specific source set. - The
actual
declaration must match theexpect
declaration's signature (parameters, return types). - 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:
// 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 platformsjvmMain
- JVM-specific codejsMain
- JavaScript-specific codenativeMain
- 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.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)