All Articles

Setting up a Kotlin multiplatform project

A Kotlin multiplatform project is a combination of two or more native applications and some shared pieces of logic. Having an iOS application and an Android application in our project means that we have an iOS target and an Android target for our shared logic.

When compiling shared logic to use in an iOS application, it’s either compiling to run on a simulator or on an actual device. Each one of those is a target and can be configured separately.

Targeting Android is slightly different. You are always building the same executable whether you are running on the emulator or on an actual device. However, some parts of your code can run directly on the JVM, if you don’t use any Android framework libraries, while other parts need the Android runtime since they use Android framework libraries.

Understanding these nuances will help in setting up a kotlin multiplatform module for code sharing between an Android and an iOS native applications.

This blog assumes that you have used Android & Xcode templates to create your native applications and you have both of them in the same folder. You should have a build.gradle file looking something likes this in your root folder.

allprojects {
    repositories {
        maven("https://dl.bintray.com/kotlin/kotlin-eap/")
        jcenter()
        google()
        mavenCentral()
    }
}

buildscript {
    extra.apply {
        set("kotlin_version","1.4.0-rc")
    }

    repositories {
        maven("https://dl.bintray.com/kotlin/kotlin-eap/")
        jcenter()
        google()
        mavenCentral()
    }

    dependencies {
        classpath("com.android.tools.build:gradle:4.2.0-alpha07")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProject.extra.get("kotlin_version")}")
    }
}

tasks.register("clean", Delete::class) {
    delete(rootProject.buildDir)
}

And you should have a folder structure similar to:

- Root
|-> androidApp
|-> iOS App
|-> build.gradle.kts
|-> settings.gradle.kts
|-> local.properties
|-> gradlew
...(other default files generated by Android Studio)

Creating a shared Kotlin module

You can use the default template provided by IntelliJ to create a shared module but let’s see how can we go about creating it without the template.

Let’s start by creating a folder for this shared module and call it shared and add a new build.gradle.kts file to configure builds for this shared module. You should end up with a similar folder structure.

- Root
|-> androidApp
|-> iOS App
|-> build.gradle.kts
|-> settings.gradle.kts
|-> shared
    |-> build.gradle.kts

Also modify your `settings.gradle.kts` file to include this new model.

include(":androidApp",  ":data")

The gradle configuration for this shared module starts by defining the plugins needed to build this module. We will need the kotlin-multiplatform plugin.

plugins {
  kotlin("multiplatform")
}

If you are going to use one the Android framework libraries, then make sure to add the Android library plugin as well

plugins {
  ...
  id("com.android.library")
}

There are two main parts in configuring the Kotlin multiplatform plugin. First, we need to configure compilation targets for this shared module. We are going to focus on targeting Android (as a kotlin only library) and iOS. Then, we can configure dependencies for those targets source sets.

Configuring iOS target

If you are working with Kotlin < 1.4, you will need to pay close attention to whether you are targeting a real device or a simulator. A real device uses iosArm64 (since it runs on ARM processor) preset while the simulator uses iosX64 (since it runs on your computer processor) preset. A common trick to determine which configuration to use to check the system environment variables. If there is an environment variable called SDK_NAME that starts with iphoneos then we are running on a real device, otherwise we are running on the simulator.

kotlin {
  targets {
    //select iOS target platform depending on environment variables
    val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
            if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
                ::iosArm64
            else
                ::iosX64
  }
}

If you are using Kotlin >= 1.4, we can leverage hierarchical project structure support and just define an ios() target which will create both iosArm64 and iosX64 with the same shared source sets.

kotlin {
  targets {
    ios() 
  }
}

Remember that you will need

kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.native.enableDependencyPropagation=false

in your

gradle.properties

to use hierarchical project structure support


When an iOS target is built, the output is a framework which can be imported in Xcode or packaged as a Cocoapods. The output framework can still be configured inside the framework block.

kotlin {
  targets {    
    ios //or iOSTarget {
      binaries {
        framework {
          baseName = "custom-name" // for example changing the output framework name
        }
      }      
    }
  }
}

Configuring JVM/Android target

When using the shared module in an Android application, you need to decide whether it is a pure java/kotlin module or an Android based module. Adding a target for either is simple

kotlin {
  targets {
    jvm("android") // for java/kotlin modules only
    android() //for an Android base module
  }
}

Remember that when building an Android module, you will have to configure the Android gradle plugin inside the android{} block.

android {
// configure the android library plugin here
}
kotlin {
  targets {
    android()
  }
}

Configuring source sets dependencies

Usually when using Kotlin multiplatform dependencies, they will have a different artefact for each platform. For example `coroutines` has

org.jetbrains.kotlinx:kotlinx-coroutines-core // for common code
org.jetbrains.kotlinx:kotlinx-coroutines-android // for android targets
org.jetbrains.kotlinx:kotlinx-coroutines-native // for native targets

We can specify the dependency for each target by customising sourceSets for each target.

kotlin {
  sourceSets {
    val commonMain by getting {
      dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:<version>")
      }
    }
    val androidMain by getting {
      dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>")
      }
    }
    val iosMain by getting {
      dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-native:<version>")
      }
    }
  }
}

Kotlin 1.4 hierarchical project structure support, allows us to define the dependency once, for example in the commonMain, and the appropriate artefact will be used for iosMain and androidMain.

Here’s an example for adding Coroutines

kotlin {
  sourceSets {
    val commonMain by getting {
      dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:<version>")
      }
    }
    val androidMain by getting {
      dependencies {}
    }
    val iosMain by getting {
      dependencies {}
    }
  }
}

Using the shared module

Both the Android project and the iOS project need to be configured to use this shared module. In case of the Android project, this configuration is a simple gradle import in androidApp/build.gradle.kts

dependencies {
  implementation(project(":shared"))
}

This will ensure that the shared code is available for the Android application and that it is compiled every time the Android project is compiled.

For iOS, we will need to first include a way for us to re-compile and build the iOS framework every time the iOS project is built then we will need to configure the project in Xcode to include the framework. Jetbrains has a great tutorial to set this up which you can find here.

However, the summary is that you need a new gradle task to build the iOS framework. Here’s the version I am currently using (heavily influenced from the Jetbrains tutorial).

val packForXcode by tasks.creating(Sync::class) {
    val targetDir = File(buildDir, "xcode-frameworks")

    /// selecting the right configuration for the iOS 
    /// framework depending on the environment
    /// variables set by Xcode build
    val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
    val sdkName: String? = System.getenv("SDK_NAME")
    val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
    val framework = kotlin.targets
            .getByName<KotlinNativeTarget>(
                if(isiOSDevice) {
                    "iosArm64"
                } else {
                    "iosX64"
                }
            )
            .binaries.getFramework(mode)
    inputs.property("mode", mode)
    dependsOn(framework.linkTask)

    from({ framework.outputDirectory })
    into(targetDir)

    /// generate a helpful ./gradlew wrapper with embedded Java path
    doLast {
        val gradlew = File(targetDir, "gradlew")
        gradlew.writeText("#!/bin/bash\n"
                + "export 'JAVA_HOME=${System.getProperty("java.home")}'\n"
                + "cd '${rootProject.rootDir}'\n"
                + "./gradlew \$@\n")
        gradlew.setExecutable(true)
    }
}

You can use it from Xcode by adding a build step to run this task early in the build process. You also need to link the framework itself in Link binary with libraries .

Adding a run script and linking the shared module in Xcode build phases

Make sure to tell Xcode to embed it when building the application else you will get a crash during runtime.

Embedding the shared module framework in Xcode