Hierarchy structure in KMM: Sharing platform code between iOS and MacOS

13 September, 2021

Recently I came across the hierarchy structure feature, a Kotlin Mobile Multiplatform setting that enables you to share platform specific code between similar platforms. In this post I will explain what this useful feature does and help you set it up.

As an example, lets take one of my recent KMM side projects. It has 3 target platforms: Android, iOS and MacOS. In it, I have the need to generate a UUID as an identifier in the shared code. This is something not available in Kotlin standard library, but available in each target platform.

There are libraries that provide this functional already, but it is something very simple to implement by ourselves using the expect/actual construct:

// commonMain/generateUUID.kt
expect fun generateUUID(): String

// androidMain
actual fun generateUUID(): String = UUID.randomUUID().toString()

// iosMain
actual fun generateUUID(): String = NSUUID().UUIDString

// macOSMain
actual fun generateUUID(): String = NSUUID().UUIDString

The Android implementation uses Java UUID class. The code for iOS and MacOS uses Foundation NSUUID class. The way the project is setup right now, we end up with a duplicate actual declaration for the NSUUID call.

This is exactly the type of problem the hierarchy structure feature solves. You can declare an intermediate source set that holds the shared platform code which the target platforms will depend on.

The documentation page explains how to enable support for this. You have to add the following option to gradle.properties:

kotlin.mpp.enableGranularSourceSetsMetadata=true

You can then use the target shortcuts the KMM plugin gives you or, in our case, you have to setup the targets manually. The shortcuts available don't actually cover our case of shared code between MacOS and iOS.

Here is the sources in build.gradle.kts for the shared module, named nativeMain. Note the creating {} call, instead of just getting():

kotlin {
    android()

    ios()
    macosX64("macos")

    cocoapods {
        summary = "Shared Framework"
        homepage = "Shared"
    }

    sourceSets {
        val commonMain by getting
        val androidMain by getting
        val iosMain by getting
        val macosMain by getting
        val nativeMain by creating {
            dependsOn(commonMain)
            iosMain.dependsOn(this)
            macosMain.dependsOn(this)
        }
    }
}

With this setup, our project is now using an hierarchy structure for our MacOS and iOS targets, and we no longer need to duplicate our actual declarations:

// generateUUID.kt
expect fun generateUUID(): String

// androidMain
actual fun generateUUID(): String = UUID.randomUUID().toString()

// nativeMain
actual fun generateUUID(): String = NSUUID().UUIDString

Hope this information helps with your KMM setup, it really helped me clean up some code duplication in my project.

Thank you for reading

Please get in touch if you have a suggestion, spot an issue or want to say hello.