Hello, Android & iOS

In this tutorial, we’re going to create a multiplatform project with Kotlin code running on both Android and iOS. Before starting, you’ll need to know the very basics of:

Open Android Studio and create a new Empty Activity project. Make sure to include Kotlin support. Next, create a new directory called common in your project root. Open settings.gradle and add it to the list of includes, so it looks like this:

include ':app', ':common'

We’ve just created a new module named common. Sync the project.

Both app and common module require the Kotlin plugin, so adjust the root build.gradle. Move the buildscript closure into the allproject closure, so the script looks like this:

allprojects {
    buildscript {
        ext.kotlin_version = '1.3.0'
        repositories {
            google()
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:3.2.1'
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        }
    }
    repositories {
        google()
        jcenter()
    }
}

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

Sync and build the project to ensure nothing is broken.

The common directory will contain code shared between both platforms. To create a multiplatform module, we first need to tell Gradle about it. Following the docs, create a new file called build.gradle in the common directory, and add this:

apply plugin: 'kotlin-multiplatform'

kotlin {
    targets {
        final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \
                              ? presets.iosArm64 : presets.iosX64

        fromPreset(iOSTarget, 'iOS') {
            compilations.main.outputKinds('FRAMEWORK')
        }

        fromPreset(presets.jvm, 'android')
    }

    sourceSets {
        commonMain.dependencies {
            api 'org.jetbrains.kotlin:kotlin-stdlib-common'
        }

        androidMain.dependencies {
            api 'org.jetbrains.kotlin:kotlin-stdlib'
        }
    }
}

// workaround for https://youtrack.jetbrains.com/issue/KT-27170
configurations {
    compileClasspath
}

Ensure the sync is successful. If you check your build logs, you should see this:

Multiplatform Sync

which means Gradle recognized our multiplatform project. Just as every other Gradle plugin, the kotlin-multiplatform plugin expects source code in certain directories. Create a new file, common/src/commonMain/kotlin/Common.kt and add the following:

expect fun platformName(): String

fun sayHello(): String = "Hello, ${platformName()}"

The code should look familiar, with the exception of one keyword: expect. It denotes a platform-specific declaration, where a common module expects all specified targets in build.gradle to implement it. Expected declarations are not restricted to methods, you can apply it to interfaces, classes, annotations, variables, etc.

The common/build.gradle has two platforms or targets: iOS and Android. Therefore the Kotlin/Native compiler expects both platforms to implement the platformName method, which is highlighted by this lint warning:

Expect Warning

Let’s handle Android first. Create a new file: common/src/androidMain/kotlin/AndroidCommon.kt and add the following:

actual fun platformName(): String = "Android"

Note the actual keyword - this is how you tell the compiler this is the actual implementation of the expected declaration. If you go back to the Common.kt file, the lint warning should now only complain about the common_iOSMain target, which means we’ve successfully covered one platform.

Now create a file common/src/iosMain/kotlin/IosCommon.kt. Before adding any code, make sure to open the Gradle tool window, and refresh the common module:

Gradle Refresh

This ensures the common module knows about the external iOS libraries provided by Kotlin/Native. Finally, add some code:

import platform.UIKit.UIDevice

actual fun platformName(): String = UIDevice.currentDevice.name

Again, note the actual keyword. More importantly, check the import statement. We’re importing UIDevice, which wouldn’t be very interesting if it weren’t for the fact that UIDevice is part of UIKit, Apple’s framework for event-driven user interface for iOS and tvOS apps. As explained in the Kotlin/Native introduction, libraries such as UIKit are available out of the box, with a single import statement.

Checking Common.kt, all lint warning should now be gone. Our common module is ready to be consumed. Open app/build.gradle and add common as a project dependency:

dependencies {
    implementation project(':common')
    // ...
}

Then, add an id to the TextView in activity_main.xml, perhaps something like android:id="@+id/hello". To avoid a quirky Android Studio behavior, build the project. Once successfully built, sync the app module. Then, open MainActivity.kt and update the TextView:

import kotlinx.android.synthetic.main.activity_main.*
import sayHello

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

        hello.text = sayHello()
    }
}

Run the app. Because common/src/androidMain/kotlin/AndroidCommon.kt specifies platformName to return "Android", we should be greeted with a Hello, Android message once the app is launched.

Hello, Android

To use the common module on iOS, there are a few extra steps we need to take. First, add the following task to the common/build.gradle:

task packForXCode(type: Sync) {
    final File frameworkDir = new File(buildDir, "xcode-frameworks")
    final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'

    inputs.property "mode", mode
    dependsOn kotlin.targets.iOS.compilations.main.linkTaskName("FRAMEWORK", mode)

    from { kotlin.targets.iOS.compilations.main.getBinary("FRAMEWORK", mode).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, then rebuild the common project. Inspect the build folder once built, and ensure xcode-frameworks directory is in it.

Xcode Frameworks

Next, create a new Xcode project. Choose a Single View app, let the product name be e.g. ios, and select the Android project root as the directory for the source code. Then add the xcode-frameworks/common-framework as an embedded binary:

Embedded Binary

Because Kotlin/Native produces native binaries and not LLVM bitcode, we need to tell that to Xcode:

Bitcode

Now we need to - again - tell Xcode where to look for frameworks. This should be a relative path from ios root directory to the xcode-frameworks directory, for example: $(SRCROOT)/../common/build/xcode-frameworks.

Search Paths

Finally, go to Build Phases, add a New Run Script Phase, drag it all the way to the top, and add the following code:

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

Run Script

Then, build the project. After it is successfully built, open ViewController.swift and replace its content with the following:

import UIKit
import common

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 21))
        label.center = CGPoint(x: 160, y: 285)
        label.textAlignment = .center
        label.font = label.font.withSize(25)

        label.text = CommonKt.sayHello()

        view.addSubview(label)
    }
}

Note the import common statement. This is the actual common module we created at the beginning of this tutorial, using nothing but Kotlin. Also, CommonKt.sayHello() should be familiar - it’s the method we defined in the Common.kt class.

Running the project on an iPhone XR iOS Simulator, the result is - unsurprisingly - a "Hello, iPhone XR" greeting:

Hello, iOS