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.
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.
MyFirstKMPProject
com.kprakash.kmp.workshop
composeApp/build.gradle.kts
and comment out the following://listOf(
// iosX64(),
// iosArm64(),
// iosSimulatorArm64()
// ).forEach { iosTarget ->
// iosTarget.binaries.framework {
// baseName = "ComposeApp"
// isStatic = true
// }
// }
composaApp/src/desktopMain/kotlin/your-pkg/main.kt
and click on the Run Icon and select Run option.composeApp
with the Android icon+
icon and select ios ApplicationUnamed
to iosApp
iosApp/iosApp.xcodeproj
iphone 16 | iOS 18.2
(this will depend what ios build tools you have installed)org.gradle.java.home
to java 23 path wasmJsBrowserRun
(source: https://kotlinlang.org)
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.
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:
We can declare common dependencies that support all targets our application support.
We can also declare platform specific dependencies for platform specific source sets.
Let's make a weather application which will have the following features:
This example will help us explore following in the context of multiplatform development:
ktor
for making network callskotlinx.serialization
for serialization and deserialization of data models.sqldelight
for local data persistencehttps://open-meteo.com
weather APIcoroutines
How to use coroutines in multiplatform appskoin
Dependency injection for multipaltform appsAdd the following to your gradle/libs.versions.toml
file
[versions]
blockkoin = "4.0.4" # > 4.x required for WASM support
ktor = "3.1.2"
kotlinxSerialization = "1.8.0"
coroutines = "1.10.1"
[libraries]
blockcoroutines-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" }
[plugins]
blockkotlinx-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 allow you to access platform-specific APIs from Kotlin Multiplatform modules. You can provide platform-agnostic APIs in the common code.
Let's create a new package
and create a new file DI
and PlatformModule
in commonMain
source set.
Hover over platformModule
and click on Add missing actual declerations
Select the platforms as shown below
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()
}
}
}
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()
) { }
}
}
}
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()
}
}