Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
51: Kotlin Multiplatform Basics
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
commonMainsource set, declare anexpectfunction namedgetCurrencySymbol()that returns aString. - In the
androidMainsource set, provide theactualimplementation that returns the symbol for the US Dollar ("$") usingjava.util.Currency.getInstance("USD").getSymbol(). - In the
iosMainsource set, provide theactualimplementation that returns the symbol for the US Dollar ("$") usingplatform.Foundation.NSNumberFormatter(or simply return the hardcoded string "$" if you are simulating the environment). - Create a shared function in
commonMaincalledformatPrice(amount: Double)that callsgetCurrencySymbol()and returns a string like "$10.00".
There are no comments for now.