Picture this: you’re tasked with building an app that runs smoothly on both Android and iOS. You’re excited to flex your coding skills, but then reality hits—writing separate codebases for each platform sounds like a one-way ticket to headache city. Duplicating logic, keeping everything in sync, and praying you don’t miss a bug on one side? No thanks. That’s where Kotlin Multiplatform (KMP) swoops in like a superhero, letting you share most of your code while still tackling those pesky platform-specific bits. And the real magic? The expect/actual trick. Grab a coffee, and let’s dig into how it works—I’ll throw in some code, a few laughs, and plenty of practical tips to make your life easier.
What’s Kotlin Multiplatform All About?
If you haven’t played with KMP yet, here’s the quick rundown. It’s a way to write code once in Kotlin and have it run on multiple platforms—Android, iOS, desktop, even the web if you’re feeling adventurous. The goal is to share the boring stuff (think business logic, data models, or networking) while leaving room for the things that have to be different, like platform APIs. It’s like cooking a big pot of chili for a party—everyone gets the same base recipe, but you can tweak the spice level for each guest.
The catch? Not everything translates perfectly across platforms. Android’s got its own way of doing things, iOS has its quirks, and trying to force them into one mold is a recipe for frustration. That’s where expect/actual steps in, acting like a translator who knows the local lingo for each platform.
Expect/Actual: Your New Best Friends
Imagine expect/actual as a restaurant order. In your shared code, you say, “I expect a tasty dish,” and each platform serves up its own version—maybe a taco on Android and a bagel on iOS. The “expect” part lives in your shared code, laying out what you need, while “actual” delivers the platform-specific goods. When you build your project, Kotlin’s compiler figures out which “actual” to use depending on the platform. It’s slick, simple, and saves you from writing the same thing twice.
Here’s the breakdown:
- Expect: Goes in your
commonMain
folder. It’s a placeholder saying, “I need this thing to happen.” - Actual: Lives in platform-specific folders like
androidMain
oriosMain
. It’s the real-deal implementation tailored to that platform.
Let’s jump into some examples to see it in action—because who doesn’t love a good coding demo?
A Simple Logging Example to Get Us Started
Every app needs to log stuff—whether it’s debugging messages or just proof you’re still alive during a long build. Android uses Logcat, iOS leans on NSLog, and trying to mash those into one shared function sounds like a mess. With expect/actual, we keep it clean.
In your shared code (commonMain
):
expect fun logMessage(message: String)
That’s all you need—just a promise that logging will happen somehow. Now, over in Android (androidMain
):
import android.util.Log
actual fun logMessage(message: String) {
Log.d("MyApp", message)
}
And for iOS (iosMain
):
import platform.darwin.NSLog
actual fun logMessage(message: String) {
NSLog("%@", message)
}
Boom! In your shared code, you can call logMessage("Hey, it’s working!")
, and Android spits it out to Logcat while iOS pipes it to the console via NSLog. The first time I got this running on both platforms, I felt like I’d cracked a secret code. It’s such a small thing, but it saves so much duplicate effort.
Taking It Up a Notch: Platform-Specific APIs
Logging’s nice, but let’s tackle something meatier—like accessing a database. Android’s got SQLite wrapped in a tidy Java API, while iOS often leans on the raw SQLite C API. Different tools, same goal: query some data. Expect/actual lets us define a shared interface and plug in the platform details where they belong.
In the shared code:
expect class Database {
fun query(sql: String): List<String>
}
This sets the stage: “I need a database that can run queries and give me a list of strings.” Now, for Android (androidMain
):
import android.database.sqlite.SQLiteDatabase
actual class Database actual constructor() {
private val db: SQLiteDatabase = // Imagine we’ve opened an SQLite db here
actual fun query(sql: String): List<String> {
val cursor = db.rawQuery(sql, null)
val results = mutableListOf<String>()
while (cursor.moveToNext()) {
results.add(cursor.getString(0))
}
cursor.close()
return results
}
}
And for iOS (iosMain
)—bear with me, this one’s a bit rougher since we’re dealing with C interop:
import platform.SQLite.*
actual class Database actual constructor() {
private val db: COpaquePointer? = // Pretend we’ve opened the db
actual fun query(sql: String): List<String> {
val statement: COpaquePointer? = null // Prep your SQLite statement here
val results = mutableListOf<String>()
// Use SQLite C API to run the query and grab results
return results
}
}
Okay, I’ll level with you—the iOS example is simplified. In real life, you’d wrestle with C pointers and SQLite’s API a bit more, but the idea holds: each platform gets its own flavor of database access. Then, in your shared code, you can do:
val db = Database()
val names = db.query("SELECT name FROM users")
Your app’s core logic stays neat and tidy, and the platform-specific mess stays out of sight. I’ve been burned before trying to juggle platform APIs without this trick—expect/actual is a lifesaver.
Best Practices to Keep Your Sanity Intact
This stuff is powerful, but don’t go overboard. Here’s what I’ve learned from trial and error:
- Keep expect simple: Don’t stuff a dozen methods into an expect declaration. The more you add, the trickier it gets to keep Android and iOS playing nice. Stick to what you really need.
- Don’t overdo it: KMP shines when you maximize shared code. If you’re writing expect/actual for every little thing, you might be missing the point—keep the common stuff king.
- Stay organized: Put shared logic in
commonMain
, Android stuff inandroidMain
, iOS iniosMain
. It’s like keeping your desk clean—you’ll thank yourself later. - Test everywhere: Since the shared code runs on all platforms, fire up both Android and iOS emulators. I skipped iOS testing once and spent hours chasing a bug that wasn’t even my fault. Never again.
Watch Your Step: Common Slip-Ups
Even pros trip sometimes. Here’s what to avoid:
- Missing an actual: Forget to write an actual for one platform, and the compiler will slap you with an error. In big projects, it’s easy to miss—always double-check.
- Platform bleed: Don’t try sneaking Android’s
Context
or iOS’sUIView
into shared code. It’s a no-go—keep it generic or use expect. - Overcomplicating: I once made an expect class with way too many methods. Maintaining it was a nightmare—smaller is better.
True story: I was knee-deep in a project and added an expect for push notifications. Android worked like a charm, but I blanked on iOS. The build failed, and after a frantic debug session (and a second coffee), I laughed it off. Lesson learned—cover all your bases.
Side-by-Side: How Platforms Differ
Sometimes it helps to see the differences laid out. For our logging example:
Platform | Logging Tool | Code Snippet |
---|---|---|
Android | Logcat | Log.d("MyApp", message) |
iOS | NSLog | NSLog("%@", message) |
And for the database:
Platform | Database API | What’s Different? |
---|---|---|
Android | SQLiteDatabase | Java-based, straightforward |
iOS | SQLite C API | C interop, a bit messier |
These tables are like cheat sheets—quick reminders of how expect/actual bridges the gap.
Why This Matters (and Why You’ll Love It)
Here’s the thing: Kotlin Multiplatform with expect/actual isn’t just a neat trick—it’s a game-changer. You write your app’s brain once, handle the platform quirks where you have to, and skip the chaos of separate codebases. Sure, there’s a bit of a learning curve, and you’ve got to be mindful with your implementations, but once it clicks, you’ll wonder how you ever lived without it.
If you’re new to this, start small. Try setting up logging or a basic API call with expect/actual. As you get the hang of it, you’ll spot all the ways it can streamline your work. And the Kotlin community? Absolute goldmine—tons of guides and friendly devs ready to help.
Next time you’re staring down a cross-platform project, don’t sweat it. With KMP and expect/actual in your toolbox, you’ve got this. Go build something awesome!
Dig Deeper Kotlin Multiplatform: Resources to Check Out
Want more? Here are some spots to explore:
- Official Kotlin Multiplatform Docs – Straight from the source.
- Expect and Actual Explained – A deep dive into the mechanics.
- Getting Started Guide – Perfect for kicking things off.
Happy coding, folks—may your builds be fast and your bugs be few!