How to set up Kotlin multiplatform library for Android and iOS
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'
}
testInstrumentationRunner
specifies the test runner for the project. As you may already know, all KMP projects usekotlin.test
framework which is an abstraction over specificAsserters
and test runners likeJunit
ortestNG
. This config specifies which test runner should be used. In our case, we useAndroid
flavor ofJunit
runner.- The corresponding version of the test runner is configured in the
dependencies
block. In our case, its1.0.2
. - The
sourceSets
is one of the most important configuration blocks. By default, Android sourceSets are configured asmain/java
andandroidTest/java
. However, to keep things consistent with other platforms likeiOS
, I moved all my android sources underandroidMain/kotlin
andandroidTest/kotlin
. This block basically points to those directories. - 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 thepackage
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
- Start your preferred emulator.
- 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.
- There seems to be an issue with running Android Tests in the Emulator if
androidTest/kotlin/
is empty. If that happens, the tests incommonTest
are also skipped and are not run. To fix it, make sure thatandroidTest/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
}