diff --git a/MULTIPLATFORM.md b/MULTIPLATFORM.md new file mode 100644 index 00000000000..4cd63bf220b --- /dev/null +++ b/MULTIPLATFORM.md @@ -0,0 +1,348 @@ +# Kotlin/Native in multiplatform projects + +While Kotlin/Native could be used as the only Kotlin compiler in the project, it is pretty common to combine +Kotlin/Native with other Kotlin backends, such as Kotlin/JVM (for JVM or Android targets) or Kotlin/JS +(for web and Node.js applications). This document describes recommended approaches and the best practices for such scenarios. + +Kotlin as a language provides a notion of expect/actual declarations, and Gradle, as an official Kotlin build system +augments it with the notion of multiplatform projects (aka MPP). Those two, combined together, provide a flexible +standartized [mechanism of multiplatform development](https://kotlinlang.org/docs/reference/multiplatform.html) +across various Kotlin flavours. + +Code, common amongst multiple platforms can be placed in common modules, while platform-specific code could be placed +into platform-specific modules, and expect/actual declarations can bind them together in developer-friendly way. + +Below one can find a step-by-step tutorial of creating a Kotlin multiplatform application for Android and iOS. + +## Creating multiplatform Android/iOS application with Kotlin + +To create MPP application one has to start with clear understanding which parts of an application is common for a different +targets, and which ones are specific, and organize module structure accordingly. For shared Kotlin code the common +ground consist of the Kotlin's standard library, which does include basic data structures and computational primitives, +along with expect classes with platform-specific implementation. Most frequently, such code consists of GUI, +input-output, cryptography and other APIs, available on the particular platform. + +In this tutorial, the multiplatform application will include three parts: + + * An **Android application** represented by a separate Android Studio project written in Kotlin. + * An **iOS application** represented by a separate XCode project, written in Swift. + * A **multiplatform library** containing a business logic of the application and used by both Android and iOS applications. + This library can contain both platform-dependent and platform-independent code and is compiled into a `jar`-library + for Android and in a `Framework` for iOS by Gradle. + +So, the multiplatform library will include three subprojects: + + * `common` - contains a common logic for both applications; + * `ios` - contains an iOS-specific code; + * `android` - contains an Android-specific code. + +### 1. Preparing a workspace + +Let's represent the structure described above as a directory tree. Assume that our multiplatform library is intended to +generate different greetings on different platforms. Create the following directory structure: + + application/ + ├── androidApp/ + ├── iosApp/ + └── greeting/ + ├── common/ + ├── android/ + └── ios/ + +As said above, [Gradle](https://gradle.org/) is the main build system for Kotlin thus our project will use it. + +To install Gradle refer to [this instruction](https://gradle.org/install/). Despite the fact that you can use the local +Gradle installation for building the project, it's recommended to use the +[Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) instead. To create the wrapper, install +Gradle as described above and execute `gradle wrapper` in the root directory of the project. After that you can +use `./gradlew` to run the build instead of using your local Gradle installation. + +Once the wrapper is created we need to describe the project structure in Gradle terms. To do this, create +a `settings.gradle` file in the root directory of the project and put the following snippet into it: + + include ':greeting' + include ':greeting:common' + include ':greeting:android' + include ':greeting:ios' + +Here we declare all subprojects for our `greeting` multiplatform library. All other multiplatform libraries included +in the project also must be declared here. + +Note that both iOS and Android applications are not included in the root Gradle build. They are represented by +independent builds which are managed by specific IDEs (Android Studio and XCode). Such an approach makes work with +these builds easier from these IDEs. + +As for IDE for other parts of the project, [IntelliJ IDEA](https://www.jetbrains.com/idea/) is recommended to be used. + +> Note: Kotlin/Native is not supported by IntelliJ IDEA so the only IDE to develop Kotlin/Native subprojects is +[CLion](https://www.jetbrains.com/clion/). But at the moment CLion has no Gradle integration. As a workaround you can +create a CLion Cmake project from a Kotlin/Native Gradle one. Just run `./gradlew generateCMake` for this project. It +will generate all the necessary files which are required. See +[this](https://blog.jetbrains.com/kotlin/2017/11/kotlinnative-ide-support-preview/) blog post to learn more about +Kotlin/Native support in CLion. + +As the final step create empty `build.gradle` files in the root directory of the project and in all subprojects which are +included in `settings.gradle`. After all these actions the project structure will be the following (files +generated by the Gradle wrapper are not shown): + + application/ + ├── androidApp/ + ├── iosApp/ + ├── greeting/ + │   ├── android/ + │   │   └── build.gradle + │   ├── common/ + │   │   └── build.gradle + │   ├── ios/ + │   | └── build.gradle + | └── build.gradle + ├── build.gradle + └── settings.gradle + +Now we have a basic structure of the project and can proceed to implementing of the multiplatform library. + +### 2. Multiplatform library + +We need to add buildscript dependencies to be able to use Kotlin plugins for Gradle in our build. Open +`build.gradle` in the `greeting` directory and put into it the following snippet: + + // Set up a buildscript dependency on the Kotlin plugin. + buildscript { + // Specify a Kotlin version you need. + ext.kotlin_version = '1.2.31' + + repositories { + jcenter() + maven { url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies" } + } + + // Specify all the plugins used as dependencies + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:0.7-dev-1407" + + } + } + + // Set up compilation dependency repositories for all projects. + subprojects { + repositories { + jcenter() + } + } + +Now all subprojects of the library can use Kotlin plugins. + +#### 2.1 Common subproject + +The `common` subproject contains a platform-independent code. To build it, add the following snippet in `common/build.gradle`: + + apply plugin: 'kotlin-platform-common' + + // Specify a group and a version of the library to access it in Android Studio. + // By default the project directory name is used as an artifact name thus the full dependency + // description will be 'org.greeting:common:1.0' + group = 'org.greeting' + version = 1.0 + + dependencies { + // Set up compilation dependency on common Kotlin stdlib + compile "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" + } + +Now we can write some logic available for all platforms. Create `common/src/main/kotlin/common.kt` and add some +functionality into it: + + // greeting/common/src/main/kotlin/common.kt + package org.greeting + + expect class Platform() { + val platform: String + } + + class Greeting { + fun greeting(): String = "Hello, ${Platform().platform}" + } + +Here we create a simple class using `expect`/`actual` paradigm. See details about platform-specific declarations +[here](https://kotlinlang.org/docs/reference/multiplatform.html#platform-specific-declarations). + +#### 2.2 Android subproject + +The `android` subproject contains platform-dependent implementations of `expect`-declarations we've created in the +`common` project. We compile it into a Java library which an Android Studio project can depend on. The content +of `android/build.gradle` will be the following: + + apply plugin: 'kotlin-platform-jvm' + + // Specify a group and a version of the library to access it in Android Studio. + // By default the project directory name is used as an artifact name thus the full dependency + // description will be 'org.greeting:android:1.0' + group = 'org.greeting' + version = 1.0 + + dependencies { + // Specify Kotlin/JVM stdlib dependency. + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + + // Specify dependency on a common project for Kotlin multiplatform build. + expectedBy project(':greeting:common') + } + + +As said above this subproject should include actual implementations of the common project's `expect`-declarations. +Let's write an Android-specific method: + + // greeting/android/src/main/kotlin/android.kt + package org.greeting + + actual class Platform actual constructor() { + actual val platform: String = "Android" + } + +#### 2.3 iOS subproject + +This project is compiled into an Objective-C framework using Kotlin/Native compiler. To do this, declare a framework in +`ios/build.gradle` and add an `expectedBy` dependency in the same manner as in the Android project: + + apply plugin: 'konan' + + // Specify targets to build the framework: iOS and iOS simulator + konan.targets = ['ios_arm64', 'ios_x64'] + + konanArtifacts { + // Declare building into a framework. + framework('Greeting') { + // The multiplatform support is disabled by default. + enableMultiplatform true + } + } + + dependencies { + // Specify dependency on a common project for Kotlin multiplatform build + expectedBy project(':greeting:common') + } + +As well as `android`, this project contains platform-dependent implementations of `expect`-declarations: + + // greeting/ios/src/main/kotlin/ios.kt + package org.greeting + + actual class Platform actual constructor() { + actual val platform: String = "iOS" + } + +### 3. Android application + +Now we can create an Android application which will use the library we implemented on the previous step. Open Android +Studio and create a new project in the `androidApp` directory. Android Studio generates all necessary files and +directories so we only need to add a dependency on our library. There are 2 actions we need to do: + +1. Add dependency on the library. To do this just open `androidApp/app/build.gradle` and add the following snippet in +the `dependencies` script block: + + ``` + implementation 'org.greeting:android:1.0' + ``` +2. Include `greeting` build in the Android Studio project as a part of +[composite build](https://docs.gradle.org/current/userguide/composite_builds.html). To do this, add the +following line in `androidApp/settings.gradle`: + + ``` + includeBuild '../' + ``` + Now dependencies of the application can be resolved in artifacts built by `greeting`. You also may publish the + Android part of `greeting` into some Maven repo and get it from there. In this case you don't need to set up + the composite build. + +> Note: Android Studio may fail to resolve declarations from the library added unless it's built. If you face such a +> problem, build the library by executing `./gradlew greeting:android:jar` in the root directory of the project. + +> Alternatively you can add the multiplatform library subprojects right into the Android Studio one instead of +> creating a composite build. To do this you need to declare them along with their directories in +> `androidApp/settings.gradle`: +> +>``` +>include ':greeting' +>include ':greeting:common' +>include ':greeting:android' +> +>project(':greeting').projectDir = file('../greeting') +>project(':greeting:common').projectDir = file('../greeting/common') +>project(':greeting:android').projectDir = file('../greeting/android') +>``` +> +> Now you can declare dependencies directly on projects instead of using maven-like coordinates: +> +>``` +>implementation project(':greeting:android') +>``` + +After these steps we can access our library as any other Kotlin code: + + import org.greeting.* + + /* ... */ + + fun foo() { + println(Greeting().greeting()) + } + +### 4. iOS application + +As said above the multiplatform library can also be used in iOS applications. The general approach here is the same as +in case of an Android application: we create a separate XCode project and add the library as a framework. But we need +to make some additional steps here. + +Unlike Android Studio XCode doesn't use Gradle, so we cannot just add the library as a dependency. Instead we need to +create a new framework in the XCode project and then replace its default build phases with a custom one which delegates +building the framework to Gradle. + +To do this, make the following steps: + +1. Create a new XCode project using `iosApp` as a root directory for it. +2. Add a new framework in the project. Go `File` -> `New` -> `Target` -> `Cocoa Touch Framework`. Specify the same +framework name as in `greeting/ios/build.gradle`: `Greeting`. +3. Choose the new framework in the `Project Navigator` and open `Build Settings` tab. Here we need to add a new build +setting specifying what Gradle task will be executed to build the framework for one or another platform. Create this +build setting in the `User-defined` section, name it `KONAN_TASK` and specify the following values for it depending on +the platform: + * For any iOS simulator (both debug and release): `compileKonanIos_x64` + * For any iOS device (both debug and release): `compileKonanIos_arm64` + + Replace `` with the name you specified in the library's `ios/build.gradle`. Use camel case, e.g. + for our `greeting` library these tasks will be named `compileKonanGreetingIos_x64` and + `compileKonanGreetingIos_arm64`. +4. Select the `Build phases` tab and remove all default phases except `Target Dependencies`. +5. Add a new `Run Script` build phase and put the following line into the script field: + + ``` + "$SRCROOT/../gradlew" -p "$SRCROOT/../greeting/ios" "$KONAN_TASK" --no-daemon -Pkonan.useEnvironmentVariables=true + ``` + + This script executes the Gradle build to compile the multiplatform library into a framework. Let's examine this + command in more detail. + * `"$SRCROOT/../gradlew"` - here we invoke the Gradle wrapper located in the root directory of the project. If you + use a local Gradle installation you need to invoke it instead of the wrapper. + * `-p "$SRCROOT/../greeting/ios"` - specify a path to the Gradle subproject containing the framework. + * `"$KONAN_TASK"` - specify a Gradle task to execute. The build setting created above is used here. + * `--no-daemon` - disable the [Gradle daemon](https://docs.gradle.org/current/userguide/gradle_daemon.html). This + setting allows us to workaround [this issue](https://github.com/gradle/gradle/issues/3468) related to a build + environment in Java 9. If you have Java 8 or earlier you may omit this flag. + * `-Pkonan.useEnvironmentVariables=true` - enable passing build parameters from XCode to the Kotlin/Native Gradle + plugin via environment variables. + +6. Add Kotlin sources into the framework: run `File` -> `Add files to "iosApp"...` and choose a directory with +Kotlin sources (`greeting/ios/src` in this sample). Choose the framework created as a target to add these sources to. +Do this for the common code of the library too. + +Now the framework is added and all Kotlin API are available from Swift code (note that you need to build the +framework in order to get code completion). Let's print our greeting: + + import Greeting + + /* ... */ + + func foo() { + print(GreetingGreeting().greeting()) + }