Getting started with Kotlin Multiplaform Mobile (KMM)

Kotlin Multiplatform Mobile (KMM) is a cross-platform framework to share code for Android and iOS apps.

When to use KMM?

  • It is highly recommended to write business logics in KMM, because it usually requires very limited or even no platform features.

  • We can also use KMM to access platform features, such as data accessing. For example, we can use Ktor to perform network requests, and SQLDelight to access SQLite databases. We can also use expect / actual declarations to access platform specific features.

  • KMM can also be used to implement presentation layer. For modern declarative UI frameworks such as Jetpack Compose and SwiftUI, the Model View Intent (MVI) pattern is highly recommended. For legacy UI frameworks, both MVI and the Model View Presenter (MVP) pattern could be used.

  • DO NOT use KMM for UI layer. Just don’t.

Be aware, Coroutines users!

It’s worth noting that for Coroutines, only the single-threaded code on Kotlin Native is supported in the Stable version. The multi-threaded version is available with known memory management issues. Additional details can be found here, and the latest update can be found here.

Despite of this, in my opinion, KMM is mature enough for production. So, let’s get started.

Create KMM project

First, we need to install Android Studio 4.2 or above, Xcode 11.3 or above, and the Kotlin Multiplatform Mobile plugin.

Now, we can create a KMM project from Android Studio by choosing the “KMM Application” template: Create KMM Project

Project structure

The basic KMM project consists of the three components:

  • A shared module that contains common code shared by both Android and iOS. This module is built into an Android library or an iOS framework using Gradle build system.

  • An Android module that builds into an Android app, using Gradle build system.

  • An iOS module that builds into an iOS app, using Xcode build tools. It is not connected with other parts of the project via Gradle, but instead using the shared module as a framework.

The shared module

The shared module is the essential part of a KMM project, which uses the KMM plugin to configure the target:

plugins {
    kotlin("multiplatform")
}

kotlin {
    android()
    iosTarget("ios") {}

    sourceSets {
        // code that works on both platforms, including the "expect" declarations
        val commonMain by getting {
            dependencies {
                // ...
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }

        // Android specific code, including the "actual" implementations
        val androidMain by getting
        val androidTest by getting

        // iOS specific code, including the "actual" implementations
        val iosMain by getting
        val iosTest by getting
    }
}

Android library

To configure the build for the Android library, we also need to add the plugin:

plugins {
    // ...

    id("com.android.library")
}

And also configure the android {} top-level block, e.g.:

android {
    compileSdkVersion(30)
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdkVersion(21)
        targetSdkVersion(30)
        versionCode = 1
        versionName = "1.0"
    }
}

To learn more about configuring the Android build, see the Android developer documentation.

Publish to Maven

To publish the Android library to a Maven repository, we can use the Maven Publish plugin:

plugins {
    // ...

    id("maven-publish")
}

group = "com.example.kmmsample"
version = "1.0.0"

kotlin {
    // ...

    android {
        publishLibraryVariants("release", "debug")
    }
}

Then we can consume it same way as any other Maven libraries.

iOS framework

The configuration to build the iOS framework is stored inside the ios {} block within the kotlin {} top-level block:

kotlin {
    // ...

    val iosTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
            ::iosArm64
        else
            ::iosX64

    iosTarget("ios") {
        binaries {
            framework {
                baseName = "KmmSample"
            }
        }
    }
}

To learn more about configure the iOS build, see the Kotlin Native documentation.

Publish as a Swift package

To publish the iOS framework as a Swift package, we can use the Multiplatform Swift Package plugin:

plugins {
    // ...

    id("com.chromaticnoise.multiplatform-swiftpackage") version "2.0.3"
}

multiplatformSwiftPackage {
    packageName("KmmSample")
    swiftToolsVersion("5.3")
    targetPlatforms {
        iOS { v("13") }
    }
}

The Swift package is created at shared/swiftpackage by default, and we can publish it to a Git repo with these steps. To consume it, follow the tutorial here.

Access platform-specific API

Unless you only use KMM to implement pure business logics and algorithms, you will very likely need to access platform-specific APIs or features. This can be achieved through the expect / actual declarations.

For example, we could define a log class in the commonMain source set of the shared module:

expect class PlatformLog() {
    fun log(msg: String)
}

The expect declarations are not allowed to contain any implementation, and KMM expects each platform to implement it using the actual keyword, e.g.:

// Android implementation in the androidMain source set.
import android.util.Log

actual class PlatformLog actual constructor() {
    actual fun log(msg: String) {
        Log.d("PlatformLog", msg)
    }
}

// iOS implementation in the iosMain source set.
import platform.Foundation.NSLog

actual class PlatformLog actual constructor() {
    actual fun log(msg: String) {
        NSLog(msg)
    }
}

Access shared code

Shared code, including platform-specific code, can be used by other shared code and Android app like any other regular Kotlin code, e.g.:

fun testLog() {
    PlatformLog().log("just a test")
}

On iOS, it’s exported to something like this:

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("PlatformLog")))
@interface SharedPlatformLog : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (void)logMsg:(NSString *)msg __attribute__((swift_name("log(msg:)")));
@end;

So can be accessed in Swift like this:

PlatformLog().log(msg: "just a test")

Conclusions

Even though KMM is still in alpha with moving parts especially the KMM plugin and IDE support, it has already been used by many big names in the industry. It is definitely something mobile developers should learn and adopt.

Thanks for reading, and have a lovely summer!


See also

comments powered by Disqus