Skip to Content
Course content

51: Kotlin Multiplatform Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

So, you've mastered Kotlin for a single target. Now we're hitting the big one: Kotlin Multiplatform (KMP). The pitch is always "write once, run anywhere," but as anyone who's actually shipped code knows, that's usually a lie. The reality is "share the logic, keep the platform-specific bits specific."

Let's actually build something and see where the friction is. I want to create a simple utility that returns the name of the operating system the app is currently running on. It sounds trivial, but it's the perfect way to see how KMP handles the boundary between shared code and platform APIs.

The "It doesn't compile" moment

I'll start in the commonMain source set. This is where our shared logic lives. I'm going to try to write a simple function that tells me where I am.

// In commonMain
fun getPlatformName(): String {
    // I want to call something like android.os.Build.VERSION 
    // or iOS's UIDevice.current.systemName
    return "Running on some platform" 
}

Now, if I just return a hardcoded string, it works. But that's useless. If I try to import android.os.Build inside commonMain, the IDE immediately screams at me. The compiler is basically saying, "I don't know what an Android Build is; I'm currently compiling for *every* target you've defined, including iOS and Desktop."

This is the core challenge of KMP. We want to write code in commonMain that uses a feature that only exists in androidMain or iosMain.

Setting the contract with expect

Since I can't implement the logic here, I have to change my approach. Instead of providing the implementation, I'll provide a promise. In KMP, we do this with the expect keyword.

I'll rewrite the function like this:

// In commonMain
expect fun getPlatformName(): String

Notice there's no body. No curly braces. I'm telling the Kotlin compiler: "I expect every platform target to provide its own specific implementation of this function. Just trust me for now; it'll be there by the time we link everything together."

If I try to run the project now, it still won't compile. The compiler is holding me to my word. It's saying, "You promised me an implementation for Android and iOS, and you've given me nothing."

Filling in the blanks with actual

Now we have to go to the platform-specific source sets. This is where the "multiplatform" part actually happens. I'll jump over to androidMain first. Here, I have full access to the Android SDK.

// In androidMain
actual fun getPlatformName(): String {
    return "Android ${android.os.Build.VERSION.SDK_INT}"
}

And then, I'll switch over to iosMain. Because Kotlin/Native maps Objective-C and Swift APIs to Kotlin, I can call iOS frameworks directly.

// In iosMain
import platform.UIKit.UIDevice

actual fun getPlatformName(): String {
    return UIDevice.currentDevice.systemName + " " + UIDevice.currentDevice.systemVersion
}

The actual keyword is the payoff. It tells the compiler, "Here is the specific implementation for this target that satisfies the expect declaration in commonMain."

Pulling it all together

Here is the magic part. Now that the contracts are fulfilled, I can go back to commonMain and use getPlatformName() inside any other shared logic without worrying about which platform is actually running the code.

// In commonMain
fun greetUser() {
    val platform = getPlatformName()
    println("Hello from $platform! This logic is shared, but the name is local.")
}

When I run this on an Android emulator, it prints "Android 33". On an iPhone simulator, it prints "iOS 16.4". The greetUser function doesn't know how the platform name is retrieved; it just knows that the expect contract guaranteed it would get a string back.

I've found that the biggest mistake people make is overusing expect/actual. If you can solve a problem using a library that is already multiplatform (like Ktor for networking or kotlinx.datetime for time), do that instead. Only reach for expect/actual when you absolutely need to touch a platform-specific API.




📋 Practical Task

Implementing a Shared Device Currency Formatter

You are building a shared financial utility module. You need to create a mechanism that returns the currency symbol used by the current platform. Since currency formatting is handled differently by the Android java.util.Currency API and the iOS NSNumberFormatter API, you'll need to use the expect/actual pattern.

Your Task:

  • In the commonMain source set, declare an expect function named getCurrencySymbol() that returns a String.
  • In the androidMain source set, provide the actual implementation that returns the symbol for the US Dollar ("$") using java.util.Currency.getInstance("USD").getSymbol().
  • In the iosMain source set, provide the actual implementation that returns the symbol for the US Dollar ("$") using platform.Foundation.NSNumberFormatter (or simply return the hardcoded string "$" if you are simulating the environment).
  • Create a shared function in commonMain called formatPrice(amount: Double) that calls getCurrencySymbol() and returns a string like "$10.00".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.