# Setting up Kotlin Multiplatform (+Compose)

# Kotlin Multiplatform (De)Wizard

Project Wizards can be great; they spare us from doing repetitive, boring, boilerplate tasks **over** and **over** again. However: Wizards are **no replacement for education or documentation**: I have seen so many engineers getting so used to *only using wizards* that they have no idea what their builds are *actually* doing or what certain blocks in their buildscripts are intended to do. Once shit hits the fan and something does not work out anymore, frustration builds up quickly, and tools like Gradle are blamed for the [learned helplessness](https://en.wikipedia.org/wiki/Learned_helplessness) which has anchored itself over years and years.

This blog post is intended to "de-wizard" setting up Kotlin Multiplatform projects by explaining **every step** required to create a Compose app by hand (w/o using the wizards). Every step will be described in detail, and an on-screen video tutorial/commentary will also be attached.

ℹ️ You can also watch the full 'walkthrough' here:

[https://youtu.be/fmFezt-2IBo](https://youtu.be/fmFezt-2IBo)

ℹ️ Alongside this post and videos, I also pushed this GitHub project, which contains the setup steps as individual commits in the 'main' branch:  
[https://github.com/sellmair/kotlin-multiplatform-getting-started](https://github.com/sellmair/kotlin-multiplatform-getting-started)

## Create a new (empty) project / .gitignore

In the beginning, there was nothing. Let there be a .`gitignore` file, so we can start checking in our project into VCS. We're planning to use IntelliJ and Gradle for the Kotlin Multiplatform (KMP) app. I recommend keeping the `.gitignore` file simple

```plaintext
.idea
.kotlin
.gradle
**/build
```

* `.idea`: IntelliJ likes to check in configuration files here. For Gradle projects, most of the configuration is done within Gradle, so a `Gradle sync` should be all you need. I recommend checking in files from within .idea only if you really want to share them (e.g. some useful run-configurations or shared dictionaries)
    
* `.kotlin`: Used by the Kotlin Gradle Plugin (KGP) to store project-level intermediate files, or files produced for the IDE (by e.g. transforming dependencies)
    
* `.gradle`: [Project-specific cache directory generated by Gradle.](https://docs.gradle.org/current/userguide/directory_layout.html#dir:project_root)
    
* `**/build`: Gradle (and Gradle Plugins) conventionally put the build output in a `build` folder.
    

%[https://youtu.be/fi85FaavuJw] 

---

## Creating the initial `settings.gradle.kts`

Creating the `settings.gradle.kts` file will get us started with using Gradle. The first piece we should configure is telling Gradle where to download libraries and plugins from. There are two top-level blocks to consider:

```kotlin
pluginManagement {
    repositories {
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}
```

The `pluginManagement` block will configure Gradle to download Gradle plugins from `mavenCentral`, whereas the `dependencyResolutionManagement` is configuring the repositories for downloading *libraries.*

%[https://youtu.be/-HaDkR4hz8I] 

---

## Loading the Kotlin Multiplatform Plugin

We told Gradle from where to download plugins and libraries: Let's request the Kotlin Multiplatform plugin now. Gradle's plugin loading is not straightforward as it *might* isolate certain Gradle plugins from each other (in which case you might run into hard-to-understand and debug issues). One strange looking, yet effective way of avoiding such issues is to load plugins in the **root** `build.gradle.kts` . Using `apply false` will load the plugin and define its version for all 'subprojects' / 'submodules', but not apply them.

```kotlin
plugins {
    kotlin("multiplatform") version "2.0.0" apply false
}
```

%[https://youtu.be/p3HaaYlPBiM] 

---

## Loading the Android Gradle Plugin

Similarly, the Android (Application) Gradle plugin can be loaded in the root `build.gradle.kts` file.

```kotlin
plugins {
    kotlin("multiplatform") version "2.0.0" apply false
    id("com.android.application") version "8.5.1" apply false
}
```

However, we will be unable to download the Android Gradle Plugin from Maven Central. We instead need to configure the `google()` maven repository additionally. Since Gradle will respect the order of repositories, trying to download plugins and libraries sequentially, I also recommend configuring the `google()` repository to only be used for `.*android.*` or `.*google.*` packages. This will decrease initial IDE sync and CLI build times, by avoiding many unnecessary network requests

```kotlin
repositories {
        google {
            mavenContent { 
                includeGroupByRegex(".*google.*")
                includeGroupByRegex(".*android.*")
            }
        }

        mavenCentral()
    }
```

%[https://youtu.be/L-lCKTNl66Q] 

---

## Setting up the ':app' module / Kotlin Targets

In the `settings.gradle.kts` we can use the `include(":app")` function to register a "Gradle *Subproject"* (which is also often called a *module* in the community). To configure this module, creating a `app/build.gradle.kts` file is required.

Typically the `build.gradle.kts` file starts by loading plugins. In our case, we can start loading (and now actually *applying*) the Kotlin Multiplatform, as well as the Android Application plugin.

```kotlin
plugins {
    kotlin("multiplatform")
    id("com.android.application")
}
```

*Note: Since we already 'loaded' the plugins in the root* `build.gradle.kts` *with a specific version, we are not required to declare a version here again. As we want to use those plugins in the. ':app' module,* `apply false` *is obviously also not required.*

Kotlin is capable of compiling for many *"Kotlin Targets"*: It is required to define the targets your code is supposed to build for. Since this guide is showcasing Kotlin for Android, iOS, and Desktop (JVM), the following targets will be declared:

```kotlin
kotlin {
    jvm() // <- for our Desktop app
    androidTarget() // <- Obviously to support Android

    iosX64() // <- Simulator for x64 host machines
    iosArm64() // <- physical iPhone
    iosSimulatorArm64() // <- Simulator for arm based host machines
}
```

%[https://youtu.be/daABLYBBnkc] 

---

## Setting up the Compose plugins

There are two Compose (Gradle) plugins that we should load when using the Compose framework

1. The `kotlin("plugins.compose")` plugin will load the "Compose Compiler Plugin" for Kotlin, which will do all of the magic, transforming your code while compiling Kotlin.
    
2. The `id("org.jetbrains.compose")` plugin will set up your build to support packaging your application, managing resources, ...
    

Similarly to the Kotlin Multiplatform and the Android Application Plugin, it makes sense to load those plugins in the root `build.gradle.kts` and then apply them in `app/build.gradle.kts`

```kotlin
/* root build.gradle.kts */
plugins {
    kotlin("multiplatform") version "2.0.0" apply false
    kotlin("plugin.compose") version "2.0.0" apply false
    id("com.android.application") version "8.5.1" apply false
    id("org.jetbrains.compose") version "1.6.11" apply false
}
```

```kotlin
/* app/build.gradle.kts */
plugins {
    kotlin("multiplatform")
    kotlin("plugin.compose")
    id("org.jetbrains.compose")
    id("com.android.application")
} 
```

%[https://youtu.be/O2P1AdJnQYk] 

---

## Writing the first `@Composable` function

Before we can use the `@Composable` annotation from Compose, declaring *foundational* dependencies is required. Since the compose dependencies are expected to be shared across all Kotlin targets, we can use the `commonMain` source set to add those dependencies.

```kotlin
/* app/build.gradle.kts */
kotlin {
    sourceSets.commonMain.dependencies {
        implementation(compose.foundation)
        implementation(compose.material3)
        implementation(compose.runtime)
    }
}
```

Those `compose.{xyz}` libraries will be available in the build script since we have loaded the `org.jetbrains.compose` plugin. Adding those three dependencies will not only resolve the `@Composable` annotation, but would even allow us to use the `material3` components (optional, of course).

The first composable can then be written in `src/commonMain/kotlin/`

```kotlin
package io.sellmair.app

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.sp

@Composable
fun MainScreen() {
    Text(
        "Hello from Kotlin!",
        fontSize = 48.sp
    )
}
```

%[https://youtu.be/ukv8YZYrEpQ] 

---

## Android: Creating the `MainActivity`

After we have created the `@Composable` function, we can wire Android up and create the `MainActivity`.

Code, specifically written for Android, can be placed into the `src/androidMain/kotlin` source directory. Here, we can just create a `MainActivity.kt` as usual.

However, since we want to use Compose on Android we have to perform several configurations:

### Adding Android-specific dependencies

The `androidMain` source set can be used to add dependencies, specifically for Android. The `activity-compose` and `appcompat` libraries are recommended:

```kotlin
/* app/build.gradle.kts */
kotlin {
   sourceSets.androidMain.dependencies {
        implementation("androidx.activity:activity-compose:1.9.0")
        implementation("androidx.appcompat:appcompat:1.7.0")
    }
}
```

### android.useAndroidX

Android requires to explicitly 'opt-in' when using `androidx`. Do not worry: If you forgot to do this, a nice error message with instructions will be printed.  
To opt-in, the `gradle.properties` file needs to contain the following line

```kotlin
android.useAndroidX=true
```

### Configure Android: compileSdk, namespace, applicationId

Creating an Android app requires some Android-specific configuration to be done. This includes choosing

* [`compileSdk`](https://stackoverflow.com/questions/26694108/what-is-the-difference-between-compilesdkversion-and-targetsdkversion): Which "Android version" you want to compile against (aka. which version of the APIs you want to see when coding)
    
* `minSdk`: The minimum "Android version" you want to support in your app
    
* [`targetSdk`](https://stackoverflow.com/questions/26694108/what-is-the-difference-between-compilesdkversion-and-targetsdkversion): Which "Android version" do you 'target' as in 'support all features of'.
    
* [`namespace`](https://developer.android.com/build/configure-app-module#set-namespace): Under which package shall the 'generated' code from Android be placed
    
* [`applicationId`](https://developer.android.com/build/configure-app-module#set-application-id): Unique ID for your application (suggested to be the same as `namespace`.
    

### Creating the AndroidManifest.xml

Shipping Android apps also requires declaring an `AndroidManifest.xml`. The file can be created under `app/src/androidMain/AndroidManifest.xml`

The minimal setup, to create an app, is to

* Provide a 'label' (a.k.a. a name for your app)
    
* Select a theme
    
* Declare the activity
    

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="KMP Setup Sample App"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">

        <activity android:name="io.sellmair.app.MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
        </activity>
    </application>
</manifest>
```

### Selecting a JVM Toolchain

When compiling code for Android we want to make sure that we consistently use one JVM toolchain. If not set correctly, we might be greeted with an error message like  
"*Inconsistent JVM-target compatibility detected for tasks"...*

While some teams have more complicated requirements for their JVM toolchains, most projects are very well advised to just use one consistent `jvmToolchain` for their `module`. In the app `build.gradle.kts` do:

```kotlin
/* app/build.gradle.kts */
kotlin {
    jvmToolchain(17)
}
```

%[https://youtu.be/xgjMEYxfhxQ] 

✅ After seeing the Composable on an emulator screen, we can consider the Android setup done.

---

## iOS: Building the .framework

The architecture for integrating our Kotlin code into an iOS app looks something like

\-&gt; Compile Kotlin -&gt; Build iOS .framework files -&gt; Compile Swift -&gt; Profit.

However, just declaring the iOS targets in the `kotlin {}` block will not yet build the `.framework` files from Kotlin. We need to configure the creation of those binaries for all iOS targets

```kotlin
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

// ... 

kotlin.targets.withType<KotlinNativeTarget>().configureEach {
    binaries.framework {
        baseName = "KmpApp"
        isStatic = true
    }
}
```

In this (example) case, we would like to build a 'static' framework with the name 'KmpApp' for any "KotlinNativeTarget".

When building the app (e.g by invoking `./gradlew assemble`) We should see the frameworks being located in `app/build/bin`

However, at this point it might be wise to increase the maximum amount of heap memory Gradle is allowed to allocate. Adding the following line to the `gradle.properties` would allow up to 6 GB of heap.

```plaintext
org.gradle.jvmargs=-Xmx6g
```

%[https://youtu.be/5eEl3zqHOfs] 

---

## Creating the Xcode project

When creating a new Xcode project we can select `iOS` / `App` and put the project into our `:app` module (e.g. into an `iosApp` folder)

### Create 'Compile Kotlin' run script 'phase'

As mentioned previously, we want to ensure that Kotlin produces its `.framework` before we compile our swift code against it. We can add a "Run Script Phase" to the Xcode "Build Phases" and call it "Compile Kotlin".

In this script, we are allowed to invoke the Gradle build to produce the requested `.framework` files. Using the `embedAndSignAppleFrameworkForXcode` Gradle task will allow the Kotlin Gradle Plugin to read the environment variables from Xcode, which will lead to building exactly the `.framework` for the 'configuration' currently selected by Xcode.

```bash
cd "$SRCROOT/../../../"
./gradlew :app:embedAndSignAppleFrameworkForXcode
```

  
*Note: The* `./gradlew` *invocation is prefixed by a* `cd` *command to change the current working directory to the root of the Gradle project (which also will contain the* `gradlew` *file. How you change the working directory obviously depends on the location of the Xcode project.*

### Disable "user script sandboxing"

Before we can test the Xcode build, we have to disable "user script sandboxing", as the Gradle build step is not supposed to run in a 'sandbox' as it's a grown-up part of our build chain now.

### Adding our .framework to the 'Framework Search Paths'

To compile and link against our Kotlin code (from within Xcode), we need to add the locations of our `.framework` files to the 'Framework Search Paths'.

```bash
$SRCROOT/../../build/xcode-frameworks/${CONFIGURATION}/${SDK_NAME}
```

When using the `embedAndSignAppleFrameworkForXcode`, the expected location of the framework is inside the 'build' directory under 'xcode-frameworks'. The Kotlin Gradle Plugin will use the `CONFIGURATION` and `SDK_NAME` environment variables as subdirectories (those variables will be provided by Xcode)

%[https://youtu.be/x2hcLokeQR0] 

---

## iOS: Creating/Showing the ViewController

Since we have connected Gradle (Kotlin Compile) to our Xcode project and wired everything up, we can implement the `UIViewController` which can show our Compose UI. Similarly to `androidMain`, we can write code, specifically for iOS in the source set called `iosMain`. In there, we can use the `ComposeUIViewController` function as an entry point into our Compose app.

```kotlin
/* app/src/iosMain/kotlin/.../SampleViewController.kt */
package io.sellmair.app

import androidx.compose.ui.window.ComposeUIViewController

@Suppress("unused") // Used by Swift
fun create() = ComposeUIViewController {
    MainScreen()
}
```

It is fair to suppress the 'unused code' warning in IntelliJ, as this code might only be used by Swift.

*Note: Kotlin Multiplatform Tooling in Fleet can provide Kotlin &lt;-&gt; Swift cross-language capabilities. You're welcome to* [*check this out*](https://www.jetbrains.com/help/kotlin-multiplatform-dev/fleet.html) *and provide feedback.*

The `SampleViewController.create` can now be used inside Xcode to present the UI on screen. When using SwiftUI, it is as easy as implementing a `UIViewControllerRepresentable` and displaying the `ComposeView`

```swift
import SwiftUI
import KmpApp // <- Our Kotlin Framework

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> some UIViewController {
        return SampleKmpViewControllerKt.create()
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }
}

struct ContentView: View {
    var body: some View {
        ComposeView()
    }
}
```

%[https://youtu.be/usj1Mg8wPNI] 

✅ After launching the iOS app and seeing the Compose UI on the screen, we can call the iOS setup to be done

---

## Setting up the Desktop target

Now that the application shows the UI successfully on iOS as well as Android, the remaining target to configure is the "Desktop".

Again, similar to the `androidMain` and `iosMain` source sets, the `jvmMain` source set can be used to declare dependencies, specifically for the JVM, as well as put code for the JVM only.

As of writing this article, "Compose for Desktop" requires adding one `compose.desktop` library.

```kotlin
kotlin {
   sourceSets.jvmMain.dependencies {
        implementation(compose.desktop.currentOs)
    }
}
```

After the project synced with Gradle, we can create a `Main.kt` file under `src/jvmMain/kotlin` and use the convenient `application {}` and `Window {}` functions to show our Compose UI.

```kotlin
package io.sellmair.app

import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(title = "KMP Demo", onCloseRequest = ::exitApplication) {
        MainScreen()
    }
}
```

After pressing the green 'run gutter' within the IDE, our Compose Desktop App should show on the screen.

%[https://youtu.be/Is2LOodtcD4] 

---

### Attribution

Title Wizard Hat Image by [Vectorportal.com](http://Vectorportal.com), [CC BY](https://creativecommons.org/licenses/by/4.0/)
