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:
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:
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:
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.
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.
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:
Because Kotlin/Native produces native binaries and not LLVM bitcode, we need to tell that to Xcode:
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
.
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}
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: