Understanding The Basics of Kotlin Multiplatform Projects with Kotlin 1.3

With the recently announced Kotlin 1.3 comes a massive overhaul of Kotlin Multiplatform and a new DSL for creating projects capable of sharing code between different platforms, such as Kotlin for JVM, Kotlin for JavaScript and Kotlin/Native.

In this article we’ll go through the key aspects of the new API and explain how you can use it to create projects capable of sharing code between Android and iOS.

These contents are adapted from the talk on Cross-Platform Modules with Kotlin/Native presented at Xebia’s Mois du Kotlin in October.


The new Kotlin Multiplatform API relies on a brand new Kotlin plugin for Gradle, which allows to express shared code and dependencies effectively.

In order to use the new Kotlin plugin, you simply have to add it as a dependency in your buildscript:

buildscript {
ext.kotlin_version = '1.3.0'
repositories {
jcenter()
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
}

dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

And now you‘re able to add the kotlin extension to your build.gradle file:

kotlin {
// ... your definitions here
}

The 4 Main Concepts

The Kotlin Multiplatform DSL relies on 4 main concepts:

  1. SourceSet
  2. Target
  3. Preset
  4. Compilation

Let’s go through each one of them.

1. SourceSet

The SourceSets are simply groups of source files contained in your project’s src folder.

For instance, given a simple project, you’d have the following structure.

├── build.gradle
└── src
├── commonMain // ← SourceSet
└── ... // ← Other SourceSets

In the example above, commonMain is the SourceSet which contains all the logic which is shared across your project. For instance, here you would insert the code that would run on both your iOS and Android modules.

On the other hand, iosMain is the SourceSet which contains the implementation which will be specific to your iOS framework.

Please note that one does not simply create a folder in your project’s srcand assume you’re creating a SourceSet. In fact, SourceSets must follow a specific naming imposed by Kotlin Multiplatform: we’ll detail how you can define SourceSets yourself in the second part of the article. But first, we’ll start getting familiar with the other three concepts.

2. Target

The Target is a variant of your project. A Kotlin project can support a great number of different targets: for instance, it can be used to create a JavaScript application, an Android app, an Android library, an iOS framework, a Linux executable, a JVM app, and much more, all at the same time.

In the build.gradle file, you’ll define the targets of your app by adding elements to the targets block. Those elements are called presets, and are the subject of the following section.

kotlin {
targets {
// Your targets will go here
}
}

3. Preset

Presets can be used to define preconfigured targets. In other words, Kotlin sports, out of the box, a wide range of predefined configurations allowing you to create targets with one line of code, by using thefromPreset(<PRESET_KIND>, <TARGET_NAME>) method from the DSL. While PRESET_KIND should match an existing preset made available by Kotlin, it’s up to you to define the target name you prefer. For instance, you can create an iOS simulator target for your project by adding fromPreset(presets.iosArm64, 'ios') to yourbuild.gradle file.

kotlin {
targets {
fromPreset(presets.iosArm64, 'ios')
}
}

4. Compilation

The last concept in the new Kotlin Multiplatform DSL is the compilation. The compilation, as the name implies, is a transformation of your sources to something else, such as a compiled program.

Depending on your goal, your target can use different compilations. For instance, in case you’re running your project tests, you’ll make use of the test compilation. Otherwise, in case you simply want to compile your sources, you’ll run the main compilation.

Compilations also allow you to provide a fine-grained configuration depending on the context. In other words, you can apply compilation-specific parameters to your target when building.

kotlin {
targets {
fromPreset(presets.iosX64, 'iosSim') {
compilations.main.foo = "bar1"
compilations.test.foo = "bar2"
}
}
}

SourceSet = Target + Compilation

In a previous section of the article, we’ve stated that a random folder in your project’s src does not necessarily correspond to a SourceSet. So, how can you define one?

A SourceSet is actually a folder that must match a combination of target name + compilation. In other words, assuming you have defined an ios target name and two compilations, main and test, you’ll be able to use two specific SourceSets in your src folder: iosMain and iosTest. As you might guess, files stored in those folders will be specific to those platforms and will only be available when building an iOS module or an iOS test bundle respectively.

commonMain, along with commonTest, are supported by default and are meant to contain files which should be compiled for all your supported platforms.

The case for the src/main folder

In case you’re wondering, or in case you’ve already stumbled upon a new Kotlin Multiplatform project, what about the good old src/main folder which used to contain your sources so far?

Well, it’s not going anywhere for now and you’ll still need it as it will still contain your Java files (which are not supported by platforms other than the JVM) and the AndroidManifest.xml file for Android. In other words, don’t remove your manifest from the traditional src/main folder or your project will not compile.

Target Presets for Android and iOS libraries

While we’ve described the basic keywords of the plugin, you might still ask yourself how they get applied in a real configuration targeting Android and iOS.

Configuring the Preset on Android

The preset for creating an Android library can hardly be simpler:

targets {
fromPreset(presets.android, 'androidLibrary')
}

Configuring the Preset on iOS

For iOS, things are a bit definitely more complicated, as currently there is no preset capable of supporting both the iOS Simulator and an iOS device at the same time. In order to properly configure the target, one solution is to use a variable you can pass to Gradle when building.

For instance, you can define a variable that will take the value of the platform we are building against. This can be done with Xcode, by adding a custom setting in the Build Settings pane of your iOS project and then calling Gradle from a custom build script.

You can proceed as follows.

1. From the Build Settings pane in Xcode, let’s first add a new Setting called “KOTLIN_DEVICE”, the value of which will depend on the architecture and be as follows:
- “iosSim” for “Any iOS Simulator SDK”
- “ios” for “Any iOS SDK”

2. It’s now time to add a new “Run Script” phase from the Build Phases pane in Xcode, which will call Gradle and thus run the compilation of the sources. In this phase you should also make sure to pass the KOTLIN_DEVICE setting, in order to configure the preset accordingly. The full command is as follows.

cd "<MY/KOTLIN/PROJECT>";
./gradlew linkDebugFrameworkIos -Pkotlin.device="$KOTLIN_DEVICE"

3. Finally, in the gradle script, you can add a condition which will define which preset will be applied: in case you are running an iOS Simulator, the preset value will be presets.iosX64, presets.iosArm64 otherwise.

Please note the configuration of the compilation in the preset: in case you want to create an iOS framework you should setup the output by calling the outputKind method on the main compilation (compilations.main).

targets {
// Read the "kotlin.device" variable passed by Xcode
def isSim = findProperty("kotlin.device") == "iosSim"

// Apply the preset according to the device
def iosPreset = isSim ? presets.iosX64 : presets.iosArm64
fromPreset(iosPreset, 'ios') {
// Configure the main compilation for building an iOS framework
compilations.main.outputKinds('FRAMEWORK')
}
}

Conclusion

In this article we’ve covered the foundations of the latest iteration of Kotlin Multiplatform: even if they might appear simple, these are concepts you really need to fully understand in order to start sharing Kotlin code effectively.

In a future article we’ll be going a step further and show how can you define dependencies between targets.