Skip to main content

Kotlin Platform Specifics

Introduction

When developing multiplatform applications with Kotlin, you'll inevitably need to access platform-specific APIs or implement functionality that differs across platforms. Kotlin Multiplatform provides several mechanisms to handle these differences while maintaining a clean, shared codebase.

In this guide, we'll explore how to:

  • Write platform-specific code in a multiplatform project
  • Use the expect/actual mechanism
  • Leverage platform extensions
  • Implement conditional compilation
  • Access native platform libraries

By the end, you'll understand how to effectively structure your code to maximize sharing while still accessing platform-specific capabilities.

The Expect/Actual Mechanism

The primary way to handle platform-specific implementations in Kotlin Multiplatform is through the expect/actual mechanism.

How It Works

  1. You declare an expected declaration in common code with the expect keyword
  2. You provide actual implementations for each platform with the actual keyword

Basic Example

Here's a simple example of accessing the current platform name:

kotlin
// In commonMain
expect fun getPlatformName(): String

// In androidMain
actual fun getPlatformName(): String = "Android"

// In iosMain
actual fun getPlatformName(): String = "iOS"

// In jvmMain
actual fun getPlatformName(): String = "JVM"

Now in your common code, you can call getPlatformName() and get the appropriate result for each platform.

kotlin
fun greet(): String {
return "Hello from ${getPlatformName()}"
}

Output when running on different platforms:

  • Android: "Hello from Android"
  • iOS: "Hello from iOS"
  • JVM: "Hello from JVM"

Using Expect/Actual with Classes

The expect/actual mechanism works with various Kotlin constructs, including classes:

kotlin
// In commonMain
expect class PlatformStorage {
fun saveData(key: String, value: String)
fun loadData(key: String): String?
}

// In androidMain
actual class PlatformStorage {
private val preferences = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)

actual fun saveData(key: String, value: String) {
preferences.edit().putString(key, value).apply()
}

actual fun loadData(key: String): String? {
return preferences.getString(key, null)
}
}

// In iosMain
actual class PlatformStorage {
actual fun saveData(key: String, value: String) {
NSUserDefaults.standardUserDefaults.setObject(value, forKey: key)
}

actual fun loadData(key: String): String? {
return NSUserDefaults.standardUserDefaults.stringForKey(key)
}
}

Platform Extensions

Kotlin allows you to define extensions that are available only on specific platforms.

Example: Platform-Specific Extensions

kotlin
// In androidMain
fun View.setVisibleOrGone(visible: Boolean) {
this.visibility = if (visible) View.VISIBLE else View.GONE
}

// In iosMain
fun UIView.setVisibleOrHidden(visible: Boolean) {
this.isHidden = !visible
}

These extensions are available only in the platform-specific code. You can't access setVisibleOrGone from iOS code or setVisibleOrHidden from Android code.

Conditional Compilation

Sometimes you need to include small bits of platform-specific code within a common function. Kotlin Multiplatform provides several ways to do this.

Using expect/actual Functions

kotlin
// In commonMain
fun displayAlert(message: String) {
println("Alert message: $message")
showPlatformAlert(message)
}

expect fun showPlatformAlert(message: String)

// In androidMain
actual fun showPlatformAlert(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}

// In iosMain
actual fun showPlatformAlert(message: String) {
val alert = UIAlertController(title = null, message = message, preferredStyle = .alert)
alert.addAction(UIAlertAction(title = "OK", style = .default))
rootViewController.presentViewController(alert, animated = true)
}

Using the isXxx Properties

Kotlin provides built-in properties that allow you to check the current platform:

kotlin
fun getPlatform(): String {
return when {
Platform.isAndroid -> "Android"
Platform.isIOS -> "iOS"
Platform.isJvm -> "JVM"
else -> "Unknown"
}
}

Target-Specific Source Sets

Another approach is to place code in platform-specific source sets. For example:

  • commonMain/kotlin/ - Code shared across all platforms
  • androidMain/kotlin/ - Android-specific code
  • iosMain/kotlin/ - iOS-specific code
  • jvmMain/kotlin/ - JVM-specific code

Accessing Native Libraries

One of the key advantages of Kotlin Multiplatform is the ability to access native platform libraries.

Android Example

kotlin
// In androidMain
import android.content.Context
import android.location.LocationManager

actual class LocationProvider(private val context: Context) {
actual fun getLastKnownLocation(): Location? {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)?.let {
Location(it.latitude, it.longitude)
}
}
}

iOS Example

kotlin
// In iosMain
import platform.CoreLocation.CLLocationManager
import platform.CoreLocation.CLLocationManagerDelegateProtocol

actual class LocationProvider {
private val locationManager = CLLocationManager()

actual fun getLastKnownLocation(): Location? {
return locationManager.location?.let {
Location(it.coordinate.latitude, it.coordinate.longitude)
}
}
}

Real-World Example: HTTP Client

Let's look at a real-world example of implementing an HTTP client with platform-specific networking:

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

class ApiService(private val httpClient: HttpClient) {
suspend fun fetchUserData(userId: String): UserData {
val response = httpClient.get("https://api.example.com/users/$userId")
return parseUserData(response)
}

private fun parseUserData(json: String): UserData {
// Parse JSON to UserData object
// ...
}
}

// In androidMain
import okhttp3.OkHttpClient
import okhttp3.Request

actual class HttpClient {
private val client = OkHttpClient()

actual suspend fun get(url: String): String = withContext(Dispatchers.IO) {
val request = Request.Builder().url(url).build()
val response = client.newCall(request).execute()
response.body()?.string() ?: ""
}

actual suspend fun post(url: String, body: String): String = withContext(Dispatchers.IO) {
val requestBody = body.toRequestBody("application/json".toMediaType())
val request = Request.Builder().url(url).post(requestBody).build()
val response = client.newCall(request).execute()
response.body()?.string() ?: ""
}
}

// In iosMain
import platform.Foundation.NSMutableURLRequest
import platform.Foundation.NSURLSession
import platform.Foundation.NSURL

actual class HttpClient {
actual suspend fun get(url: String): String = suspendCoroutine { continuation ->
val nsUrl = NSURL(string = url)
val request = NSMutableURLRequest(uRL = nsUrl)

NSURLSession.sharedSession.dataTaskWithRequest(request) { data, response, error ->
if (error != null) {
continuation.resumeWithException(Error("Network error"))
return@dataTaskWithRequest
}

val responseString = data?.let { NSString.create(it, NSUTF8StringEncoding) as String } ?: ""
continuation.resume(responseString)
}.resume()
}

actual suspend fun post(url: String, body: String): String = suspendCoroutine { continuation ->
val nsUrl = NSURL(string = url)
val request = NSMutableURLRequest(uRL = nsUrl)
request.setHTTPMethod("POST")
request.setHTTPBody(body.encodeToByteArray())

NSURLSession.sharedSession.dataTaskWithRequest(request) { data, response, error ->
if (error != null) {
continuation.resumeWithException(Error("Network error"))
return@dataTaskWithRequest
}

val responseString = data?.let { NSString.create(it, NSUTF8StringEncoding) as String } ?: ""
continuation.resume(responseString)
}.resume()
}
}

Best Practices

When working with platform-specific code in Kotlin Multiplatform, consider these best practices:

  1. Minimize Platform-Specific Code: Keep platform-specific code to a minimum and abstract it behind common interfaces.

  2. Use Interfaces: Define interfaces in common code and implement them for each platform, rather than using expect/actual for large classes.

  3. Create Facades: Create facade classes that hide platform complexities and expose a clean API to common code.

  4. Separate Business Logic: Keep business logic in common code, separate from platform-specific implementations.

  5. Test Each Platform: Write tests for each platform-specific implementation to ensure they behave consistently.

Summary

Kotlin Platform Specifics provide powerful mechanisms to handle platform-dependent code in multiplatform projects:

  • Expect/Actual declarations let you define a common API with platform-specific implementations
  • Platform extensions allow you to add platform-specific functionality to existing types
  • Conditional compilation enables you to include platform-specific code within common functions
  • Target-specific source sets organize your code based on platform requirements

By effectively using these tools, you can maximize code sharing while still leveraging the full power of each platform's native capabilities.

Additional Resources

Exercises

  1. Create a simple Kotlin Multiplatform project that displays different UI elements on Android and iOS while sharing business logic.

  2. Implement a platform-specific file storage system using the expect/actual mechanism.

  3. Create a networking module that uses platform-specific HTTP clients (OkHttp on Android, NSURLSession on iOS) behind a common API.

  4. Design a location service that accesses the platform's geolocation capabilities while exposing a common interface to shared code.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)