Getting started with Kotlin MultiPlatform

How has Kotlin been working out for you in your Android projects? What if I tell you Kotlin can be used to build multi-platform apps and to be very clear I am not talking about just iOS and Android but even JavaScript apps. Amazing right?

In this blog, we are going to discuss,

  • What is Kotlin Multiplatform?
  • How does Kotlin Multiplatform work?
  • How to set up your Kotlin Multiplatform project?
  • How can we share code across platforms?

So, before any delay let us get started.

What is Kotlin Multiplatform ?

When we build an app for our startup, we write iOS or Android apps most commonly. We might have design changes but more or less we have the same core logic for the apps. So, in place of writing two apps, Kotlin Multiplatform provides a way to share common business logic and build apps for different platforms using Kotlin.

Kotlin Multiplatform is a kotlin language feature that allows us to run Kotlin in JavaScript, iOS, and native desktop applications and hence develop apps using Kotlin. In short,

Kotlin Multiplatform says that, it will take care of the buisness logic and we just need to take care of the UI.

To summarise, Kotlin Multiplatform is a way to share code between different platforms.

How does Kotlin Multiplatform work?

Here, if you can visualize when we write the Kotlin Shared code for the platforms and then it compiles it gets automatically generated for the specific platform. For example, if you compile on JVM the other code will run on Android or any platform JVM platform.

So, basically, it re-uses the business logic which is almost common for all the apps.

How to set up your Kotlin Multiplatform project?

Now, from this section, we will dive into the code and set up the project. To start off, we have to install IntelliJ CE Version and set up in your local machine. In my case, I am using Macbook. After running it, I will see

Step 01.

Run the IDE and select Mobile Android/iOS from the section below, and then press next.

Step 02.

Then we get to this screen, we just need to process ahead.

Step 03.

In this, you need to set the specific path where you want to store the project and just press the finish. Your project will be set up.

Now, when the setup is done your project will throw an error like the following,

This basically means that we need to set the path of our SDK in the local.properties file. For me, the Path was

sdk.dir=/Users/himanshusingh/Library/Android/sdk
Tip: If you already have any android project of yours, just check their local.properties and you will find the path of the directory

This is how we can set up the project in Kotlin MultiPlatform. This is just a beginning.

Let us now modularize the project. For this project, we will first take the project and import it using Android Studio. You can download and set up the Android Studio from here.

Let's import the project and go to the project structure in Android Studio.

Here, in the app folder, we would have

In the above code, src contains all the code. For this project, we will delete all the modules at first and then will start creating our own folder/packages.

Now, we will rename the app folder as shared. And then we will start modularising the codebase.

We will create now, 4 folders in src of shared named as androidMain, iosMain, commonMain, main. Now, let's create a kotlin folder in the first three of them and AndroidManifest.xml in the main folder The new structure will look like,

Now, this is all. For the shared project. So, basically, we have two projects shared and iosApp, one contains the above folders and iosApp which we got when we created the project.

Now, we need to create an android project. So, for that, we will create an android project using Android Studio somewhere else in our system with an Empty Activity(say MainActivity) and quickly copy in the multiplatform project folder we created as androidApp package. So, now the final multiplatform project looks like,

Here, androidApp is the android project we created using Android Studio and pasted it here. Now, edit the settings.gradle file and replace include line with,

include ':androidApp', ':shared'

So, now let's do some coding in the multiplatform projects.

First, we need to set up the build.gradle of a shared project. We will replace with the following,

plugins {
    id 'org.jetbrains.kotlin.multiplatform'
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    buildTypes {
        release {
            minifyEnabled false
        }
    }
}

kotlin {
    android("android")
    // This is for iPhone emulator
    // Switch here to iosArm64 (or iosArm32) to build library for iPhone device
    targets {
        final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \
                          ? presets.iosArm64 : presets.iosX64

        fromPreset(iOSTarget, 'ios') {
            binaries {
                framework()
            }
        }
    }
    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 {
        }
    }
}

// This task attaches native framework built from ios module to Xcode project
// (see iosApp directory). Don't run this task directly,
// Xcode runs this task itself during its build process.
// Before opening the project from iosApp directory in Xcode,
// make sure all Gradle infrastructure exists (gradle.wrapper, gradlew).
task copyFramework {
    def buildType = project.findProperty('kotlin.build.type') ?: 'DEBUG'
    def target = project.findProperty('kotlin.target') ?: 'ios'
    dependsOn kotlin.targets."$target".binaries.getFramework(buildType).linkTask

    doLast {
        def srcFile = kotlin.targets."$target".binaries.getFramework(buildType).outputFile
        def targetDir = getProperty('configuration.build.dir')
        copy {
            from srcFile.parent
            into targetDir
            include 'shared.framework/**'
            include 'shared.framework.dSYM'
        }
    }
}

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

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

tasks.check.dependsOn iosTest

Here, we have added a plugin for multiplatform and library. We added the plugin library just so that it can be imported into the Android project androidApp.

kotlin {
}

In this, we add the device configuration where I can run the apps to test. We have added iOS and android using,

android("android")
// This is for iPhone emulator
// Switch here to iosArm64 (or iosArm32) to build library for iPhone device
targets {
    final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \
                      ? presets.iosArm64 : presets.iosX64

    fromPreset(iOSTarget, 'ios') {
        binaries {
            framework()
        }
    }
}

Again,

sourceSets {
    
}

contains the dependency for the individual projects as specified above.

Now, to integrate the shared module in the android app I will implement it as a library of Android,

implementation project(':shared')

Let's Run our first Hello World Program :)

Create a file Sample.kt in shared/kotlin folder and add,

object Sample {
    fun sayHelloWorld(): String = "Hello World"
}

in the androidApp module, we have an Activity folder (MainActivity.kt) when we created the project using Android Studio. The Activity File will have its XML file with a textView like this

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

and in the MainActivity.kt, I will just set the text to this textView in onCreate() like,

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        text.text = Sample.sayHelloWorld()

    }
}

So, when we run the Android app, it will generate the output

This is how we can run the android app in the emulator. In the above example, we just shared a common logic to print "Hello World" which is just a string representation.

How we can share code across platforms?

What if we have to share some set of code, which is only platform-specific. So for example to demonstrate,

if(platform == "iOS"){

}else if(platform =="android") {
    
}

Here we need to do something like this, like other cross-platform. But in Kotlin we have key words like actual and expect.

To explain to you better, let's say I want to print the Android Version the app is running on. This can't be done from the common package of shared.

Let's first goto shared->main->AndroidManifest.xml and add the following,

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="<my project package>"/>

So, to get the full implementation of the platform-specific code, kotlin has two keywords actual and expect. Let's understand them,

in Sample.kt of shared/commonMain module, I will add

expect fun platformName(): String

object Sample {
    fun sayPlatformName(): String = platformName()
}

and in Sample.kt of shared/androidMain module, we will do

import android.os.Build

actual fun platformName(): String = "Android ${Build.VERSION.SDK_INT}"

Here, you can see the Sample.kt of androidMain we have a function called platformName() with the actual keyword which returns the Version of Android API. It gets imported from android.os.Build package.

and in the Sample.kt of commonMain, we create function platformName() with expect keyword. and in the object we have sayPlatformName() with returns the platformName() .

And in the MainActivity of the android project, we call the sayPlatformName as,

override fun onCreate(savedInstanceState: Bundle?) {
    ....
    text.text = Sample.sayPlatformName()

}

and when we run this,

It generates the output with the version code i.e. 28.

But what exactly is happening here? Let's break it down step by step.

  • expect , specifies that the function name is expected from the platform-specific code.
  • actual, exports the code from the platform with the same function name which expect function is expecting in the common shared code.
  • All actual declarations that match any part of an expected declaration need to be marked as all actual.
  • expect function will never any implementation.

The pictorial description of expect-actual is,

So this is how we can write platform-specific code for android, iOS or JS.

That is how you set up a Multiplatform project and work with it.

Happy learning.

Team MindOrks :)

Also, Let’s connect on Twitter, Linkedin, Github, and Facebook