Creating your first Multiplatform project with Kotlin/Native (iOS & Android)
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:
- Target Dependencies
- 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 sharedclass 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