Kotlin Multiplatform (De)Wizard

Step by step

We will use IntelliJ and Gradle for the Kotlin Multiplatform (KMP) app.

Create a new (empty) project / .gitignore

Use IntelliJ IDEA and create empty project. So, File -> New Project and in left menu section pick Empty project. Give it a name, pick project location and at the end check Create Git Repository.

Click on File -> New File and give it a name: .gitignore and keep .gitignore file simple:

.idea
.kotlin
.gradle
**/build
  • .idea: for configuration files
  • .kotlin: used by the Kotlin Gradle Plugin (KGP) – project level intermediate files.
  • .gradle: project specific cache directory generated by Gradle.
  • **/buld: for build outputs.

Create settings.gradle.kts file

We have to tell Gradle where to download libraries and plugins from.

pluginManagement {
    repositories {
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}

include(":app")

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

Loading the Kotlin Multiplatform Plugin

Create new file: build.gradle.kts. We will 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.

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

Loading the Android Gradle Plugin

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

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

Expect to see something like:

Plugin [id: 'com.android.application', version: '8.5.1', apply: false] was not found in any of the following sources:

We can’t download the Android Gradle Plugin from Maven Central. Instead we need to configure the google maven repository additionally. We will configure the google repository to be only used for .*android.*, .*google.* or .*androidx.* packages. This will decrease initial IDE sync and CLI build times, by avoiding many unnecessary network requests. Open settings.gradle.kts and paste next code:

pluginManagement {
    repositories {
        google {
            mavenContent {
                includeGroupByRegex(".*google.*")
                includeGroupByRegex(".*android.*")
                includeGroupByRegex(".*androidx.*")
            }
        }

        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositories {
        google {
            mavenContent {
                includeGroupByRegex(".*google.*")
                includeGroupByRegex(".*android.*")
                includeGroupByRegex(".*androidx.*")
            }
        }

        mavenCentral()
    }
}

include(":app")

Setting up the ‘:app’ module / Kotlin Targets

In settings.gradle.kts we are using include(“:app”) function to register a “Gradle Subprojet” or module. To configure this module, we have to create a app/build.gradle.kts file.

So, File -> New Directory -> app and in there create build.gradle.kts file.

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

kotlin {
    jvm() // <- for Desktop app
    androidTarget() // <- for Android

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

If you now see:

Cause: compileSdkVersion is not specified. Please add it to build.gradle

Do next:

Configure Android: compileSdk, namespace, applicationId

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

  • compileSdk: 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: Which “Android version” do you ‘target’ as in ‘support all features of’.
  • namespace: Under which package shall the ‘generated’ code from Android be placed
  • applicationId: Unique ID for your application (suggested to be the same as namespace).
/* Android Configuration */
android {
    compileSdk = 34
    namespace = "me.mitkovic.kmp.currencyconverter"
    defaultConfig {
        minSdk = 29
        applicationId = "me.mitkovic.kmp.currencyconverter"
    }
}

Setting up the Compose plugins

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

  • 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.
  • The id(“org.jetbrains.compose”) plugin will set up your build to support packaging your application, managing resources, …

We will load those plugins in root build.gradle.kts file and apply them in app build.gradle.kts file:

/* 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
}
/* app/build.gradle.kts */
plugins {
    kotlin("multiplatform")
    kotlin("plugin.compose")
    id("org.jetbrains.compose")
    id("com.android.application")
}

Writing the first @Composable function

In app folder create /src/commonMain/kotlin folder.

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 commonMain source set to add those dependencies.

/* app/build.gradle.kts */
kotlin {
    jvm() // <- for Desktop app
    //... just add code below into current kotlin block. You can do it at the bottom of the block.
    sourceSets.commonMain.dependencies {
        implementation(compose.foundation)
        implementation(compose.material3)
        implementation(compose.runtime)
    }
}

The first composable can then be written in app/src/commonMain/kotlin + package (me.mitkovic.kmp.currencyconverter in my case). Create MainScreen.kt file and paste the code:

package me.mitkovic.kmp.currencyconverter

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
    )
}

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 + package source directory. Here, we can just create a MainActivity.kt file.

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:

/* app/build.gradle.kts */
kotlin {
    jvm() // <- for Desktop app
    //... just add code below into current kotlin block. You can do it at the bottom of the block.
    
   sourceSets.androidMain.dependencies {
        implementation("androidx.activity:activity-compose:1.9.0")
        implementation("androidx.appcompat:appcompat:1.7.0")
    }
}

android.useAndroidX

In project root folder add gradle.properties file and the following line:

android.useAndroidX=true

Our MainActivity will look like this:

package me.mitkovic.kmp.currencyconverter

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity

class MainActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            MainScreen()
        }
    }
}

Creating the AndroidManifest.xml

Shipping Android apps also requires declaring an AndroidManifest.xml. The file can be created under app/src/androidMain/AndroidManifest.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="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:

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

Run the project in Android emulator or physical device and you will see: Hello from Kotlin!

Setting up the Desktop target

Similar to the androidMain source set, the jvmMain source set can be used to declare dependencies, specifically for the JVM, as well as put code for the JVM only.

Create src/jvmMain/kotlin folder and package in it: me.mitkovic.kmp.currencyconverter

“Compose for Desktop” requires adding one compose.desktop library.

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 + package, and use the convenient application and Window functions to show our Compose UI.

package me.mitkovic.kmp.currencyconverter

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

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

And that is it. Run your project by pressing the green ‘run gutter’ within the IDE, in front of: fun main() = application {

Compose Desktop App should be show on the screen.

iOS: Building the .framework

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

-> Compile Kotlin -> Build iOS .framework files -> Compile Swift -> 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 (in app/build.gradle.kts):

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

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

In this example, 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 2048M of heap.

org.gradle.jvmargs=-Xmx2048M

Creating the Xcode project

When creating a new Xcode project we can select iOS / App, give it a name KmplosApp (different than KmpApp used for KotlinNativeTarget in app/build.gradle.kts) and put the project into our root project module (e.g. into an iosApp folder).

Create ‘Compile Kotlin’ run script ‘phase’

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“.

So, go to our project in Xcode, select project name (KmplosApp), and then on in the right panel select Build Phases and create new Run Script Phase (click on + below Build Phases toolbar). Put it above Compile sources because we wont first Kotlin compiler to produce the framework and then we want to use this framework which can be used in Swift code to run the app.

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.

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

Add the code above in the edit box with current text:
# Type a script or drag a script file from your workspace to insert its path.

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.

We can now click on Start button in Xcode to start creating our frameworks.

And now we see sandboxing error.

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.

Go to Build Settings and type sandboxing and disable it for User Script sandboxing. Disable it for both Project and Targets by clicking on KmplosApp on the left and pick No for User Script Sandobxing in Build Options.

Then go to project in Intellij IDEA and type in Terminal:

./gradlew --stop

to stop any gradle deamon that currently running.

Click again on Start button in Xcode to start creating our frameworks and to lunch the app on your phone or simulator. The build shall be successful now.

Adding our .framework to the ‘Framework Search Paths’

Now we have to tell Xcode now where to look for frameworks.

For that we will go to Build Settings and search for framew to shorten search results. Go for Search Paths -> Framework Search Paths and add:

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

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.

So, in IntelliJ IDEA, create iosMain/kotlin in src folder and in there create the same package as you did before: me.mitkovic.kmp.currencyconverter. In there create new SampleViewController.kt file

/* app/src/iosMain/kotlin/.../SampleViewController.kt */
package me.mitkovic.kmp.currencyconverter

import androidx.compose.ui.window.ComposeUIViewController

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

Now switch back to Xcode and open ContentView.swift file.

The ampleViewControllerKt.SampleViewController 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.

import SwiftUI
import KmpApp // <- Our Kotlin Framework

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> some UIViewController {
        return SampleViewControllerKt.SampleViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

    }
}

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

If we run this in Xcode we will see that Hello from Kotlin is showing up.

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

Thanks Sebastian Sellmair for your guidance!
https://blog.sellmair.io/setting-up-kotlin-multiplatform-compose

Leave a Reply