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
- You declare an expected declaration in common code with the
expect
keyword - You provide actual implementations for each platform with the
actual
keyword
Basic Example
Here's a simple example of accessing the current platform name:
// 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.
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:
// 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
// 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
// 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:
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 platformsandroidMain/kotlin/
- Android-specific codeiosMain/kotlin/
- iOS-specific codejvmMain/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
// 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
// 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:
// 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:
-
Minimize Platform-Specific Code: Keep platform-specific code to a minimum and abstract it behind common interfaces.
-
Use Interfaces: Define interfaces in common code and implement them for each platform, rather than using expect/actual for large classes.
-
Create Facades: Create facade classes that hide platform complexities and expose a clean API to common code.
-
Separate Business Logic: Keep business logic in common code, separate from platform-specific implementations.
-
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
- Official Kotlin Multiplatform Documentation
- Kotlin Multiplatform Mobile Developer Portal
- KMM Samples Repository
Exercises
-
Create a simple Kotlin Multiplatform project that displays different UI elements on Android and iOS while sharing business logic.
-
Implement a platform-specific file storage system using the expect/actual mechanism.
-
Create a networking module that uses platform-specific HTTP clients (OkHttp on Android, NSURLSession on iOS) behind a common API.
-
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! :)