Creating your first Multiplatform project with Kotlin/Native (iOS & Android)

Daniel Reyes Sánchez
7 min readMay 16, 2019

--

Nowadays there are lots of possibilities to create projects to target multiple platforms. But most of them involve learning other languages or technologies and also at some time at development, you need to mess around with some configurations on each platform project depending on the complexity.

With Kotlin Multiplatform, an eager team of Android and iOS developers can use what they know to share modules across both platforms without having to learn a new technology or language, speeding up the business layer of the application.

By using Kotlin/Native, you can develop the core of the app with a single code base. Perform unit testing and distribute a shared module to both platforms.

Repository of this project: https://github.com/DanielReyesDev/HelloKotlinNative

The initial project

Since we’re depending on Kotlin for our shared module, It makes sense to start creating the Android project.

Go ahead and create a new project and make sure that Kotlin is selected as Language and “Use androidx.* artifacts” is checked.

Wait for the project to sync and update libraries if needed. Then build and run to see if everything is working.

That’s it, we have our brand new Android Project.

Separating-out our Android Project

In this case, we will reuse some of the autogenerated boilerplate created by Android Studio for setting up our Kotlin/Native project. So first we will separate out our Android specific code:

Close the Android project and rename “app” folder to “androidApp

It should look like the following:

Update settings.gradle file to be like this:

include ':androidApp'

This will make reference to our renamed project instead of the default one

Clean the project and remove ./androidApp/app.iml

Sync if needed from File > Sync Project with Gradle Files. For some reason, sometimes the Android configurations could be lost and hence you cannot run the project. So this will fix all the missing parts.

Build and run again to make sure everything is right.

Setting up the Kotlin/Native multiplatform project

After separating-out the Android project, we will need to add some configurations and adjustments to our project in order to compile properly the Kotlin/Native code.

First, start by adding new directories to organize the multiplatform project:

mkdir -p shared/src/commonMain/kotlin
mkdir -p shared/src/androidMain/kotlin
mkdir -p shared/src/iosMain/kotlin
mkdir -p shared/src/main

And also add these new files:

touch shared/src/commonMain/kotlin/common.kt
touch shared/src/androidMain/kotlin/android.kt
touch shared/src/iosMain/kotlin/ios.kt
touch shared/src/main/AndroidManifest.xml
touch shared/build.gradle

In the end, it should look like this:

After doing that, you should update the ./build.gradle file

Start by defining a Kotlin native version:

ext.kotlin_native_version = '1.3.21'

Then add the required repositories:

maven { url 'https://plugins.gradle.org/m2' }
maven { url 'http://dl.bintray.com/kotlin/kotlin-eap' }
maven { url 'https://dl.bintray.com/jetbrains/kotlin-native-dependencies' }

Now add Kotlin Native dependency:

classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$kotlin_native_version"

The complete build.gradle file will look similar to this after all modifications:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
ext.kotlin_version = '1.3.21'
ext.kotlin_native_version = '1.3.21'
repositories {
maven { url 'https://plugins.gradle.org/m2' }
maven { url 'http://dl.bintray.com/kotlin/kotlin-eap' }
maven { url 'https://dl.bintray.com/jetbrains/kotlin-native-dependencies' }
google()
jcenter()

}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$kotlin_native_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
google()
jcenter()

}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

Sync now

Now we have to update our ./settings.gradle file in order to include all the new folders and .kt files, but that’s an easy one:

include ':shared'
include ':androidApp'

After doing that you’ll see a build.gradle file inside our brand new shared folder, but it’s empty, so we will update it …

updating ./shared/build.gradle file with some boilerplate code:

apply plugin: 'com.android.library'
apply plugin: 'kotlin-multiplatform'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 15
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
kotlin {
targets {
final def iOSTarget = System.getenv('SDK_NAME')?.startsWith('iphoneos') ? presets.iosArm64 : presets.iosX64
fromPreset(iOSTarget, 'ios'){
binaries {
framework('shared')
}
}
fromPreset(presets.android, 'android')
}
sourceSets {
// for common code
commonMain.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib-common'
} androidMain.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib'
}
iosMain.dependencies { }
}
}
configurations {
compileClasspath
}

Update ./shared/src/main/AndroidManifest.xml file with minor boilerplate code:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="YOUR.PACKAGE.HERE"/>

Sync now

Build and run

Writing the Kotlin/Native shared module

After all manual configurations to our Kotlin/Native project is time to start writing our code:

Add the greeting expect fun in the common.kt file

package com.bosch.hellokotlinnativeexpect fun platformName(): Stringclass Greeting {
fun greeting(): String = "Hello, ${platformName()}"
}

Then add the greeting actual fun in the android.kt file

package com.bosch.hellokotlinnativeimport android.os.Buildactual fun platformName(): String {
return "Android ${Build.VERSION.RELEASE}"
}

Then add the greeting actual fun in the ios.kt file

package com.bosch.hellokotlinnativeimport platform.UIKit.UIDevice
actual fun platformName(): String {
return "${UIDevice.currentDevice.systemName()} ${UIDevice.currentDevice.systemVersion()}"
}

Wrapping-up:

Next add some packaging options in ./androidApp/build.gradle

android {......packagingOptions {
exclude 'META-INF/*.kotlin_module'
}
}

And then in the same file, add the shared dependencies:

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':shared')
......}

Sync now

Updating TextView to show corresponding platform name on Android:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/greeting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

package com.bosch.hellokotlinnativeimport androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.bosch.hellokotlinnative.Greeting
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
greeting.text = Greeting().greeting()
}
}

Now build and run your Android Project and see the results:

The iOS Project

Go ahead and create a brand new iOS Project inside the iosApp and make sure to use Swift as our language.

Now, we have to go back to Android Studio in order to add a Gradle task that compiles the Kotlin code into a framework.

Under ./shared/build.gradle add the following task at the end:

......task packForXCode(type: Sync) {
final File frameworkDir = new File(buildDir, "xcode-frameworks")
final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
final def framework = kotlin.targets.ios.binaries.getFramework("shared", mode)
inputs.property "mode", mode
dependsOn framework.linkTask
from { framework.outputFile.parentFile }
into frameworkDir
doLast {
new File(frameworkDir, 'gradlew').with {
text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
setExecutable(true)
}
}
}
tasks.build.dependsOn packForXCode

Sync now.

then go to the terminal, dive into the project root folder and execute the following:

./gradlew :shared:build

this will start the packaging task with Gradle in order to compile Kotlin code into a binary so we can import it into our brand new iOS Project.

Switch back to your iOS Project again and go to Build Phases tab then click on the little plus icon at the top left to add a new build phase and choose New Run Script Phase

And add the following script

cd "$SRCROOT/../../shared/build/xcode-frameworks"
./gradlew :shared:build -PXCODE_CONFIGURATION=${CONFIGURATION}

It should look like this:

Important: Make sure your build phases are in the correct order:

  1. Target Dependencies
  2. Run Script

Then go to Build Settings and search for “Enable Bitcode” and change it to “No

In the same tab look for “Framework Search Paths” and you have to double click on the right to add a new path.

A pop-up will appear and you’ll have to click on the plus icon and paste the following path:

$(SRCROOT)/../../shared/build/xcode-frameworks

By doing this: first our iOS Project will ask Gradle to generate our binary .framework and then our Xcode project will be prepared to look on the right folder where the framework lies.

In the end, you just need to press enter to confirm.

Now we have to add the reference of the actual shared.framework

Go to the General tab. Click on the plus icon on Embedded Binaries, select Add Other… and look for the .framework outside the iOS Project folder in order to import it.

The framework should be located in ../../shared/build/xcode-frameworks

Note: if you didn’t run the ./gradlew command, you won’t get the shared.framework.

After doing that, select create folder references, since every time we build the iOS Project, it will ask Gradle to rebuild the .framework file.

Build the project…

Now add a label on the center of the default view controller laying on the Main.storyboard and drag the IBOutlet to the ViewController.swift.

Import the shared module and use our shared class:

import UIKit
import shared
class ViewController: UIViewController {@IBOutlet weak var greetingLabel: UILabel!override func viewDidLoad() {
super.viewDidLoad()
greetingLabel.text = Greeting().greeting()
}
}

Congratulations, you have built your first Kotlin Native Project 🎉

Repository of this project: https://github.com/DanielReyesDev/HelloKotlinNative

--

--