Hands-on workshop for Mobile Development using Kotlin Multiplatform and Compose Multiplatform for building Android, iOS, and web applications.
You will learn to set up a multiplatform project, how we can share code between platforms, how to handle situations where code cannot be shared between platforms, and what are the challenges and benefits of using the Kotlin multiplatform.
Through practical exercises, we will discover some best practices for efficient development, and at the end of the workshop, at the very least everyone will be equipped with some knowledge of KMP and how we can develop mobile or web applications with shared code and UI.

Intro

General knowledge of Kotlin and Jetpack Compose (https://developer.android.com/compose) will be a nice to have.

Don't worry if you're not familiar with any of these, we will spend some time at the begning of the workshop to familiarize ourselves with some basic building blocks of Kotlin Multiplatform & Compose Multiplatform.

Requirements

iOS Requirements (only if you want to support iOS)

  1. Download starter project using multiplatform wizard
    1. We will use kotlin multipaltform wizard to download a starter project and use that to develop our application. The starter project can be downloading from this Link (Link)
    2. Give your project a name. For ex: MyFirstKMPProject
    3. Provide a project ID. For ex: com.kprakash.kmp.workshop
    4. Select the following platforms
      1. Android
      2. iOS
      3. Desktop
      4. Web
    5. Download
    6. Unzip the project
  2. Open the project in Android Studio
    1. It might take few minutes for the project to import and gradle sync to finish
    2. If you run into java incompatibility issues, make sure you have the correct java version set under IDE settings > Build, Execution, Deployment > Build Tools > Gradle
  3. Disable iOS (optional, if you don't have a mac or have not setup ios build tools)
    1. Go to composeApp/build.gradle.kts and comment out the following:
//listOf(
//        iosX64(),
//        iosArm64(),
//        iosSimulatorArm64()
//    ).forEach { iosTarget ->
//        iosTarget.binaries.framework {
//            baseName = "ComposeApp"
//            isStatic = true
//        }
//    }
  1. Desktop Run
    1. Go to composaApp/src/desktopMain/kotlin/your-pkg/main.kt and click on the Run Icon and select Run option.
  2. Android Run
    1. In the run configuration, select composeApp with the Android icon
  3. ios Run
    1. Go to Edit Configurations (tap shift key 2 times and type edit configurations)
    2. Click on the + icon and select ios Application
    3. Change Unamed to iosApp
    4. For Xcode project file, choose iosApp/iosApp.xcodeproj
    5. Execution Target iphone 16 | iOS 18.2 (this will depend what ios build tools you have installed)
    6. Click on apply and hit run
    7. If you run into Java version issues, open gradle.properties file and set org.gradle.java.home to java 23 path Intro
  4. Browser Run (WASM)
    1. Open the gradle tasks list on the right panel
    2. Double click on wasmJsBrowserRun

(source: https://kotlinlang.org)

Targets

Targets define the platforms to which Kotlin compiles the common code. These could be, for example, the JVM, JS, Android, iOS, or Linux. The previous example compiled the common code to the JVM and native targets.

A Kotlin target is an identifier that describes a compilation target. It defines the format of the produced binaries, available language constructions, and allowed dependencies.

targets

Platform sourcesets

A Kotlin source set is a set of source files with its own targets, dependencies, and compiler options. It's the main way to share code in multiplatform projects.

Each source set in a multiplatform project:

sourcesets

We can declare common dependencies that support all targets our application support.

We can also declare platform specific dependencies for platform specific source sets.

dependencies

What will the app do?

Let's make a weather application which will have the following features:

What will we learn from this?

This example will help us explore following in the context of multiplatform development:

Add the following to your gradle/libs.versions.toml file

koin = "4.0.4" # > 4.x required for WASM support
ktor = "3.1.2"
kotlinxSerialization = "1.8.0"
coroutines = "1.10.1"
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel", version.ref = "koin" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" }
ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktor" }
ktor-client-java = { group = "io.ktor", name = "ktor-client-java", version.ref = "ktor" }
ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktor" }
kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

Add the serialization plugin to root build.gradle.kts

plugins { 
   ....
   alias(libs.plugins.kotlinx.serialization) apply false   
}

Add serialization plugin to composeApp/build.gradle.kts

plugins {
   ....
   alias(libs.plugins.kotlinx.serialization)
}

Add dependencies:

commonMain.dependencies {
   ...
   implementation(libs.koin.compose.viewmodel)
   implementation(libs.koin.core)
   implementation(libs.coroutines.core)
   implementation(libs.kotlinx.serialization)
   implementation(libs.ktor.client.core)
}
androidMain.dependencies {
   implementation(libs.ktor.client.android)
   implementation(libs.koin.android)
}

desktopMain.dependencies {
   ...
   implementation(libs.kotlinx.serialization)
   implementation(libs.ktor.client.java)
}
iosMain.dependencies {
   ...
   implementation(libs.ktor.client.darwin)
}
wasmJsMain.dependencies {
   ...
   implementation(libs.ktor.client.js)
}

In order to get started we will first explore how to setup koin for dependency injection, through this we will learn how we inject common dependencies as well as platform specific dependencies like ktor http client which is different for each platform.

Expected and actual declarations

Expected and actual declarations allow you to access platform-specific APIs from Kotlin Multiplatform modules. You can provide platform-agnostic APIs in the common code.

Declaring DI initialization and Platform specific modules

Let's create a new package .di and create a new file DI and PlatformModule in commonMain source set.

Hover over platformModule and click on Add missing actual declerations

platform-module

Select the platforms as shown below

actual-platforms

actual fun platformModule(): Module {
    return module {
        single<HttpClientEngine> {
            Android.create()
        }
    }
}
actual fun platformModule(): Module {
   return module {
      single<HttpClientEngine> {
         Java.create()
      }
   }
}
actual fun platformModule(): Module {
   return module {
      single<HttpClientEngine> {
         Darwin.create()
      }
   }
}
actual fun platformModule(): Module {
   return module {
      single<HttpClientEngine> {
         Js.create()
      }
   }
}

Koin Initializer

In DI file under commonMain sourceset add following:

import io.ktor.client.HttpClient
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module

fun initKoin(appDeclaration: KoinAppDeclaration = {}): KoinApplication {
   return startKoin {
      appDeclaration()
      modules(commonModule(), platformModule())
   }
}

// called by iOS etc
fun initKoin() = initKoin() {}

private fun commonModule(): Module {
   return module {
      single {
         HttpClient(
            engine = get()
         ) {  }
      }
   }
}

Initialize koin for each platform

Create an Application class and add it to manifest

import android.app.Application
import com.kprakash.kmp.workshop.di.initKoin

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        initKoin()
    }
}

Edit composeApp/desktopMain/pkg/main.kt

import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.kprakash.kmp.workshop.di.initKoin

private val koin = initKoin().koin

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        title = "MyFirstKMPProject",
    ) {
        App()
    }
}

Edit iosApp/iosApp/iOSApp.swift

import SwiftUI
import ComposeApp

@main
struct iOSApp: App {
    init() {
        KoinKt.doInitKoin()
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Edit composeApp/wasmJsMain/pkg/main.kt

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import com.kprakash.kmp.workshop.di.initKoin
import kotlinx.browser.document

private val koin = initKoin().koin

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
    ComposeViewport(document.body!!) {
        App()
    }
}