Skip to main content

Command Palette

Search for a command to run...

Build Your First Kotlin Multiplatform App: A Guided Tour

Published
6 min read
O

I am an adventurous person who finds beauty in solving problems using technology. I am passionate about startups and innovative solutions, building developer communities, sharing my knowledge and passion for technology. Helping people gain valuable experience through learning opportunities. I am also a smart and reliable individual with the zeal to add value while developing the required competence. Good at working in a diverse work environment, aimed at meeting desired organizational objectives.

The dream of "write once, run anywhere" has been a siren song for developers for decades. While many frameworks have tried, few have struck the right balance between shared code and native performance. Kotlin Multiplatform (KMP), a modern, pragmatic approach from JetBrains that doesn't try to replace everything. Instead, it lets you share the code that makes sense—your business logic, data models, and network calls—while leaving the UI layer fully native.

If you're curious about KMP but don't know where to start, you're in the right place. We're going to take a guided tour inspired by Google's official "Build your first KMP app" codelab. We'll build a simple app that counts the days since a certain event, demonstrating the core principles that make KMP so powerful.

The Project: A Simple "Days Since" Counter

Our goal is to build an app for both Android and iOS that shows a simple greeting and calculates how many days have passed in the current year. It's a trivial task, but it’s perfect for showcasing how to:

  1. Share core logic written in pure Kotlin.

  2. Handle platform-specific APIs with KMP's expect/actual mechanism.

  3. Share UI-adjacent components like ViewModels.

  4. Even share assets like images across platforms.

The Heart of KMP: The shared Module

When you create a new KMP project in Android Studio, you'll immediately notice the shared module. This is where the magic happens. It's structured to hold code for different targets:

  • commonMain: This is for your 100% platform-agnostic Kotlin code. The logic, data classes, and interfaces you write here will be compiled for Android, iOS, and any other platform you target.

  • androidMain: Code here can access Android-specific APIs (like Context, Log, etc.). This is where you'll implement platform-specific features for Android.

  • iosMain: Similarly, this is for iOS-specific code and can access native iOS/Apple frameworks like Foundation or UIKit.

Sharing the Obvious Stuff: The Business Logic

Let's start with the easy part. Our app needs to calculate the number of days that have passed in the year. This is pure logic and has nothing to do with Android or iOS. So, we place it right in commonMain.

Generated kotlin

      // In shared/src/commonMain/kotlin/Greeting.kt

import kotlin.math.abs

class Greeting {
    private val platform: Platform = getPlatform()

    fun greet(): String {
        val days = daysUntilNewYear()
        return "Guess what it is! > ${platform.name.reversed()}!" +
                "\nThere are only $days days left until New Year! 🎆"
    }
}

The daysUntilNewYear() function (not shown for brevity) is just a date calculation. It's written once in commonMain and will work everywhere. This is the core value proposition of KMP: share your logic, not your problems.

The Bridge to the Native World: expect and actual

But wait, how do we get the current date to perform the calculation? java.time.LocalDate isn't available in commonMain because it's a JVM-specific API. This is where KMP's most elegant feature comes into play: expect and actual declarations.

1. Define the Expectation (expect)

In commonMain, we declare that we expect each platform to provide a Platform object with a name.

Generated kotlin

      // In shared/src/commonMain/kotlin/Platform.kt
expect class Platform() {
    val name: String
}

expect fun getPlatform(): Platform

This is like defining an interface. We're saying, "I don't care how you do it, but every platform that uses this shared code must provide an actual implementation of Platform and getPlatform()."

2. Provide the Reality (actual)

Now, we provide the concrete implementations in the platform-specific source sets.

For Android (androidMain):

Generated kotlin

      // In shared/src/androidMain/kotlin/Platform.kt
import android.os.Build

actual class Platform {
    actual val name: String = "Android ${Build.VERSION.SDK_INT}"
}

actual fun getPlatform(): Platform = Platform()

Here, we can freely access the Android SDK's Build.VERSION.SDK_INT.

For iOS (iosMain):

Generated kotlin

      // In shared/src/iosMain/kotlin/Platform.kt
import platform.UIKit.UIDevice

actual class Platform {
    actual val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun getPlatform(): Platform = Platform()

And here, we use UIKit to get the device name and OS version. The commonMain code can now call getPlatform() without ever knowing the underlying details. This powerful pattern allows you to abstract away any platform-specific dependency.

Beyond Code: Sharing Images and Resources

This is often the "Aha!" moment for new KMP developers. Sharing logic is one thing, but what about assets? Surely you need to add your images to res/drawable on Android and Assets.xcassets on iOS, right?

Not necessarily! The KMP ecosystem has fantastic libraries to solve this, and the codelab introduces a popular one: moko-resources.

Here's how it works:

  1. Add the Dependency: You add the moko-resources plugin and library to your build.gradle.kts.

  2. Place the Resource: You put your image (e.g., rocket.png) into a special folder: shared/src/commonMain/resources/drawable.

  3. Access in Shared Code: The library automatically generates a SharedRes object. You can now reference your image from commonMain, for example, in a shared ViewModel.

Generated kotlin

      // In a shared ViewModel in commonMain
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.SharedRes

class RocketLaunchViewModel : ViewModel() {
    val rocketImage: ImageResource = SharedRes.images.rocket
    // ... other logic
}

The ImageResource is a platform-agnostic identifier. The moko-resources library then provides helper functions for each platform to convert this identifier into a native, drawable image.

  • On Android (Jetpack Compose): painterResource(imageResource)

  • On iOS (SwiftUI): imageResource.toImage()

You just shared an image across platforms without ever touching the native asset folders. This is a massive win for consistency and maintainability.

Tying it all Together: The Native UIs

With our shared logic, platform-specific bridges, and even shared resources in place, the final step is to hook them up to the native UIs.

On Android with Jetpack Compose:
This feels incredibly natural. You can directly instantiate your Greeting class or a shared ViewModel and display the data.

Generated kotlin

      // In an Android @Composable function
import dev.icerock.moko.resources.compose.painterResource

// ...
Text(text = Greeting().greet())
Image(
    painter = painterResource(SharedRes.images.rocket),
    contentDescription = "Rocket"
)

On iOS with SwiftUI:
The process is very similar. The KMP toolchain automatically generates a framework from your shared module that you can import directly into Xcode.

Generated swift

      // In a SwiftUI View
import SwiftUI
import shared // Your shared module!

struct ContentView: View {
    // Helper to observe StateFlow from Swift
    @StateObject private var viewModel = RocketLaunchViewModel()

    var body: some View {
        VStack {
            Text(Greeting().greet())
            // Use the moko-resources helper
            Image(uiImage: viewModel.rocketImage.toUIImage()!)
        }
    }
}

You write your UI in pure SwiftUI, calling into the shared Kotlin code as if it were a native Swift library.

Your Journey Starts Here

By following this simple "Days Since" app, we've touched on the very essence of Kotlin Multiplatform. It’s not about abandoning native development; it's about enhancing it. You get to:

  • Share what matters: Business logic, data validation, and API definitions.

  • Stay native where it counts: The user interface, platform integrations, and overall feel.

  • Leverage a growing ecosystem: Libraries like moko-resources, Ktor for networking, and SQLDelight for databases make sharing even more powerful.

If this has piqued your interest, the best next step is to try the codelab for yourself. The hands-on experience of writing the code, seeing it compile for two platforms, and running it on both an emulator and a simulator is the best way to truly understand the power and potential of Kotlin Multiplatform. Welcome to the future of cross-platform development.

More from this blog

Untitled Publication

2 posts

I am an adventurous person who finds beauty in solving problems using technology. I am passionate about startups, innovative solutions, building developer communities, sharing my knowledge in tech.