Configure KMP
Fleet IDE is the dedicated IDE to consider for KMP developpement with exclusive features such as better preview management
Simply download it thanks to Jetbrain ToolBox App
Use Android Studio
It is also possible to use Android Studio IDE with latest stable version koala version or above. You can do the following to prepare it to support KMP
- Open Android Studio Iguana or above
- Select
Plugins
tab - search for
Kotlin Multiplatform
and click on install button - search for
Compose Multiplatform
and click on install button - restart your IDE
For macOS devs only,kdoctor
command line interface (CLI) is available. It will help you to ensure that your computer is correctly configured for KMP development.
brew install kdoctor
kdoctor
๐งช Download the initial project
For your hand-on lab today, you can download the initial project by downloading KMP official sample for Android, iOS and Desktop & Web here: kmp.jetbrains.com
- Select : โ๏ธ Android โ๏ธ iOS โ๏ธ Desktop โ๏ธ Web
Download
the zip project- Open it with
Fleet
๐ A Guided tour of the sample project
Project Structure
The gradle plugin of Kotlin Multiplatform ( KMP ) organize the code thanks to 2 essential notion of Gradle/Java :
- A
Module
is a set of classes and packages that form a complete whole with a build description filebuild.gradle
. Modules have been introduced to improve safety and to make the platform more modular. - A
Source sets
give us a powerful way to structure source code in our Gradle projects. A SourceSet represents a logical group of Kotlin source and resource files.
composeApp
module : The crossplatform library module
1 - A shared library module linked to all project platforms. It contains the source code common to all your supported platforms.
commonMain
sourceSet : Shared & multiplatform Kotlin source files
2 - This is the place where you will code all your cross platform composables.
On the sample, your first composable function App()
is already configured with a single button that display an image with a standard animation on click.
App.kt
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
}
}
}
androidMain
, desktopMain
, iosMain
, wasmJsMain
sourceSets: KMP specific library modules
3 - One submodule per platform, linked to the common module sources. It gives the possibility to make specific implementations of functions per platform
Platform specific source file
When you need a specific implementation for Android and iOS of getPlatform() to return the platform name, KMP uses :
expect
keyword on the KMP shared library (commonMain) before functions indicating that we need a specific implementation of this functionactual
keywords on the KMP shared library specific modules (iosMain, androidMain) before functions to indicate the implementation.
For exemple on this specific template, a getPlatformName
fuction is referenced on the common code and implemented specificly on each sourceset with the right platform name
platform.kt (SourceSet : commonMain)
expect fun getPlatform(): Platform
Platform.desktop.kt (SourceSet : desktopMain)
actual fun getPlatformName(): String = "Desktop"
Platform.android.kt (SourceSet : androidMain)
actual fun getPlatformName(): String = "Android"
Platform.ios.kt(SourceSet : iosMain)
actual fun getPlatformName(): String = "iOS"
More Information
On each platform sourceSet (androidMain
, desktopMain
, iosMain
, wasmJsMain
) , you can call native SDK function wrapped in Kotlin.
Ex: on Platform.ios.kt a UIDevice function is called :
UIDevice.currentDevice.systemName()
More information about platform specific functions in KMP here)
Platform specific composables
On this template a wrapper is used to use the root multiplatform composable App()
on each specific sourceSet Main
class :
onCreate
callback of anActivity
for Android- A
ViewController
class for iOS - ... Then you can code and declare your composables on the
App()
composable to code multiplatform.
For Desktop (DesktopMain)
main.desktop.kt(SourceSet : desktopMain)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Quiz",
) {
App()
}
}
For Android (AndroidMain)
The Android app declaration with ressouces, manifest and activities A MainView
android composable is created from the App() composable.
main.android.kt (SourceSet : androidMain)
@Composable fun MainView() = App()
Then the composable is declared on the activity.
MainActivity.kt (androidApp)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}
4. for iOS (IosMain)
For iOSApp
project you can open the .xcodeproj with Xcode for completion, build specific configurations
It's the same principles, a swift MainViewController
that is created from the composable App()
main.ios.kt(SourceSet : iosMain)
fun MainViewController() = ComposeUIViewController { App() }
Then on the .xcodeproj, ContentView.swift
convert the MainViewController
into a swiftUI view.
ContentView.swift (iosApp)
...
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
}
}
...
With those configuration you can now develop your composable in the commonMain
SourceSet and deploy your app for Android, iOS and Destop targets
๐งช Deploy your apps
To defines gradle configuration for deploying your development apps, you need to create a running configuration for fleet by creating a run.json
file in .fleet
folder.
.fleet/run.json
{
"configurations": [
{
"name": "composeApp",
"type": "gradle",
"workingDir": "$PROJECT_DIR$",
"tasks": [":server:classes"],
"initScripts": {
"flmapper": "ext.mapPath = { path -> path }",
"Build learning-kotlin-multiplatform-src": "System.setProperty('org.gradle.java.compile-classpath-packaging', 'true')"
}
},
{
"name": "server",
"type": "jps-run",
"workingDir": "$PROJECT_DIR$",
"dependsOn": ["composeApp"],
"mainClass": "com.worldline.quiz.ApplicationKt",
"module": "Quiz.server.main",
"options": ["-Dfile.encoding=UTF-8"]
},
{
"name": "iOS",
"type": "xcode-app",
"workingDir": "$PROJECT_DIR$",
"allowParallelRun": true,
"buildTarget": {
"project": "iosApp",
"target": "iosApp"
},
"configuration": "Debug"
},
{
"name": "wasmJs",
"type": "gradle",
"workingDir": "$PROJECT_DIR$",
"tasks": ["wasmJsBrowserDevelopmentRun"],
"args": ["-p", "$PROJECT_DIR$/composeApp"],
"initScripts": {
"flmapper": "ext.mapPath = { path -> path }"
}
},
{
"name": "android",
"type": "android-app",
"workingDir": "$PROJECT_DIR$",
"allowParallelRun": true,
"module": "quiz.composeApp.main"
},
{
"name": "Desktop",
"type": "gradle",
"workingDir": "$PROJECT_DIR$",
"tasks": ["desktopRun"],
"args": ["-DmainClass=com.worldline.quiz.MainKt", "--quiet", "-p", "$PROJECT_DIR$/composeApp"],
"initScripts": {
"flmapper": "ext.mapPath = { path -> path }"
}
}
]
}
Instead, if you want to use gradle tasks , here are some examples :
./gradlew desktopRun #Desktop
./gradlew wasmJsBrowserDevelopmentRun #Web
CORS issue for Web target
For the Web App, you can bypass CORS issue if you don't have a remote server with Chrome as below:
<google chrome path> --disable-web-security --user-data-dir=/Users/xxxx/Desktop/googlechrometmp http://localhost:8080/
Running configuration
Version Catalog
A version catalog
is a list of dependencies, represented as dependency coordinates, that a user can pick from when declaring dependencies in a build script.
gradle/libs.versions.toml
[versions]
# KMP AGP/GRADLE compatibility guide
# https://wrl.li/guideagp
kotlin = "2.0.20"
agp = "8.5.0"
compose-plugin = "1.7.0-rc01"
androidx-activityCompose = "1.9.2"
navigation = "2.8.0-alpha10"
androidx-lifecycle = "2.8.0"
kotlinxCoroutinesCore="1.9.0"
kotlinx-coroutines = "1.8.1"
kotlinxDatetime = "0.6.1"
ktorVersion = "3.0.0-rc-1"
kstore = "0.8.0"
logback = "1.5.8"
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
kotlin-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorVersion" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorVersion" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorVersion" }
ktor-client-apache = { module = "io.ktor:ktor-client-apache", version.ref = "ktorVersion" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktorVersion" }
#kstore
kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
kstore-storage = { module = "io.github.xxfast:kstore-storage", version.ref = "kstore" }
# Web
#ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktorVersion" }
# Server
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktorVersion" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktorVersion" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktorVersion" }
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktorVersion" }
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktorVersion" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ktor = { id = "io.ktor.plugin", version.ref = "ktorVersion" }
Basic logging
A logger is provided by [Ktor client library
] (https://ktor.io/docs/logging.html) for basic logs.
More advanced logging and debugging
Use can have more advanced logging and debugging thanks to third party libs such as NSExceptionKT
or CrachKiOS
or Kermit
or Napier
โ If everything is fine, go to the next chapter โ