How to set up Kotlin multiplatform library for Android and iOS

Suparn Gupta
5 min readJun 23, 2019

--

I have been working with KMP a lot recently. For all my ongoing work, check out this repo https://github.com/suparngp/kotlin-multiplatform-projects

Every tutorial or documentation I found online is about JVM and iOS. The official KMP docs on Android Libraries are only good if you know how KMP projects work in detail (for example source sets and compilations configuration and so on) which may not be beginner friendly. This post is about doing just that with minimal configuration.

Goal: Setup a Kotlin Multiplatform library which targets iOS and Android (specifically) which you can test on Simulator/Emulator and in your machine’s JVM (by mocking of course).

Directory Structure

├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
├── settings.gradle
└── src
├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── res
├── androidTest
│ └── kotlin
├── commonMain
│ ├── kotlin
│ └── resources
├── commonTest
│ ├── kotlin
│ └── resources
├── iosMain
│ ├── kotlin
│ └── resources
└── iosTest
├── kotlin
└── resources

Configuration

It is all about how you configure the build.gradle in your project. I am using IntelliJ to write all my projects. However, you can set up everything manually as well. Below is the list of code blocks which will get you all set up. Just copy and paste them in order.

1. Build script configuration

buildscript {
repositories {
jcenter()
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
}
}

This configuration affects the build script it self. Here, we are telling the build.gradle to use android gradle build tools with version 3.4.1

2. Plugins

plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.40'
}

apply plugin: 'com.android.library'

We next apply two plugins, one to make this project a KMP project and the second to create an Android Library out of it.

3. Common Gradle Configuration

repositories { // configure the repositories for your project
mavenCentral()
google() // this is where com.android.library plugin loads from
jcenter()
}

group 'com.example' // group id for maven artifact
version '0.0.1' // version of your project

4. Android Specific Configuration

android { // android specific configuration
compileSdkVersion 28 // SDK version to compile against
defaultConfig {
minSdkVersion 15 // min SDK version supported by this lib
targetSdkVersion 28 // max SDK version supported by this lib
versionCode 1
versionName '1.0'
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' // See notes #1
}
buildTypes { // the build variants for the lib
release {
minifyEnabled false
}
}
sourceSets { // See notes #3
main {
manifest.srcFile 'src/androidMain/AndroidManifest.xml'// See notes #4
java.srcDirs = ['src/androidMain/kotlin']
res.srcDirs = ['src/androidMain/res']
}
androidTest {
java.srcDirs = ['src/androidTest/kotlin']
res.srcDirs = ['src/androidTest/res']
}
}
}

dependencies { // See notes #2
androidTestImplementation 'com.android.support.test:runner:1.0.2'
}
  1. testInstrumentationRunner specifies the test runner for the project. As you may already know, all KMP projects use kotlin.test framework which is an abstraction over specific Asserters and test runners like Junit or testNG. This config specifies which test runner should be used. In our case, we use Android flavor of Junit runner.
  2. The corresponding version of the test runner is configured in the dependencies block. In our case, its 1.0.2.
  3. The sourceSets is one of the most important configuration blocks. By default, Android sourceSets are configured as main/java and androidTest/java . However, to keep things consistent with other platforms like iOS , I moved all my android sources under androidMain/kotlin and androidTest/kotlin. This block basically points to those directories.
  4. You may wonder why you need a Manifest file. Since our android lib is going be an aar, we need a manifest file at the right location with just the package name.

5. Kotlin

kotlin {
// This is for iPhone emulator
// Switch here to iosArm64 (or iosArm32) to build library for iPhone device
iosX64("ios") {
binaries {
framework()
}
}
// use the android preset
android("android") {
// you can also publish both "release" and "debug"
publishLibraryVariants("release")
}
sourceSets { // configure the source sets
commonMain {
dependencies {
implementation kotlin('stdlib-common')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
androidMain {
dependencies {
implementation kotlin('stdlib')
}
}
androidTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}

iosMain {
}
iosTest {
}


}
}

This is basically the stock Kotlin MP configuration which IntelliJ creates by default. We add dependencies for common, android and iOS for both main and test .

6. Running iOS tests in simulator

task iosTest {
def device = project.findProperty("iosDevice")?.toString() ?: "iPhone 8"
dependsOn kotlin.targets.ios.binaries.getTest('DEBUG').linkTaskName
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Runs tests for target 'ios' on an iOS simulator"

doLast {
def binary = kotlin.targets.ios.binaries.getTest('DEBUG').outputFile
exec {
commandLine 'xcrun', 'simctl', 'spawn', device, binary.absolutePath
}
}
}

This task will compile and link your iOS framework and run the tests against it in a simulator. All tests under commonTest and iosTest source sets will run together. This is also configured by IntelliJ by default.

7. Extras

configurations { // Seems to be a hack for a gradle bug.
compileClasspath
}

8. AndroidManifest.XML

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.suparnatural.sample">

</manifest>

Just in case you are wondering what to put in AndroidManifest.xml , you can use the above snippet with your package name.

Running Tests on Emulator

  1. Start your preferred emulator.
  2. In the library root, run ./gradlew connectedAndroidTest

You can also run tests in the Machine’s JVM by mocking android specific code first and then running ./gradlew testDebugUnitTest .

Workarounds (Will be updated)

Below are some of the workarounds for common problems which I haven’t been able to address only by configuration even though build.gradle seems to be okay. I will keep updating this section with any other fixes as I keep finding them.

  1. There seems to be an issue with running Android Tests in the Emulator if androidTest/kotlin/ is empty. If that happens, the tests in commonTest are also skipped and are not run. To fix it, make sure that androidTest/kotlin has at least one .kt file. The file may be empty, and just needs to be present.

Here is the complete build.gradle at a glance.

buildscript {
repositories {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
}
}

plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.40'
id 'maven-publish'
}

apply plugin: 'com.android.library'

repositories {
mavenCentral()
google()
jcenter()
}

group 'com.example'
version '0.0.1'

android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName '1.0'
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
buildTypes {
release {
minifyEnabled false
}
}
sourceSets {
main {
manifest.srcFile 'src/androidMain/AndroidManifest.xml'
java.srcDirs = ['src/androidMain/kotlin']
res.srcDirs = ['src/androidMain/res']
}
androidTest {
java.srcDirs = ['src/androidTest/kotlin']
res.srcDirs = ['src/androidTest/res']
}
}
}

dependencies {
androidTestImplementation 'com.android.support.test:runner:1.0.2'
}

kotlin {
// This is for iPhone emulator
// Switch here to iosArm64 (or iosArm32) to build library for iPhone device
iosX64("ios") {
binaries {
framework()
}
}
android("android") {
// you can also publish both "release" and "debug"
publishLibraryVariants("release")
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
androidMain {
dependencies {
implementation kotlin('stdlib')
}
}
androidTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}

iosMain {
}
iosTest {
}


}

}

task iosTest {
def device = project.findProperty("iosDevice")?.toString() ?: "iPhone 8"
dependsOn kotlin.targets.ios.binaries.getTest('DEBUG').linkTaskName
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Runs tests for target 'ios' on an iOS simulator"

doLast {
def binary = kotlin.targets.ios.binaries.getTest('DEBUG').outputFile
exec {
commandLine 'xcrun', 'simctl', 'spawn', device, binary.absolutePath
}
}
}

configurations {
compileClasspath
}

--

--

Responses (1)