From 0cf944bb321cc6e94ef59573162f7e8d4d4deea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Thu, 27 Aug 2026 14:16:09 +0200 Subject: [PATCH 1/3] Add web and desktop support --- .github/workflows/build-desktop.yml | 19 +++++ .github/workflows/build-web.yml | 19 +++++ README.md | 2 +- androidApp/module.yaml | 2 - desktopApp/module.yaml | 9 +++ desktopApp/src/main.kt | 18 +++++ iosApp/module.xcodeproj/project.pbxproj | 14 ++-- .../xcshareddata/xcschemes/app.xcscheme | 80 +++++++++++++++++++ iosApp/module.yaml | 2 - kotlin | 4 +- kotlin.bat | 17 ++-- libs.versions.toml | 1 + project.yaml | 2 + shared/module.yaml | 6 +- shared/src/screens/PlaceholderColor.kt | 8 ++ shared/src/screens/detail/DetailScreen.kt | 4 +- shared/src/screens/list/ListScreen.kt | 4 +- webApp/module.yaml | 10 +++ webApp/resources/index.html | 17 ++++ webApp/resources/styles.css | 41 ++++++++++ webApp/src/main.kt | 14 ++++ 21 files changed, 268 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/build-desktop.yml create mode 100644 .github/workflows/build-web.yml create mode 100644 desktopApp/module.yaml create mode 100644 desktopApp/src/main.kt create mode 100644 iosApp/module.xcodeproj/xcshareddata/xcschemes/app.xcscheme create mode 100644 shared/src/screens/PlaceholderColor.kt create mode 100644 webApp/module.yaml create mode 100644 webApp/resources/index.html create mode 100644 webApp/resources/styles.css create mode 100644 webApp/src/main.kt diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml new file mode 100644 index 0000000..2542bf7 --- /dev/null +++ b/.github/workflows/build-desktop.yml @@ -0,0 +1,19 @@ +name: Build desktop app + +on: + push: + branches: + - kotlin-toolchain + pull_request: + +jobs: + test: + name: Build + runs-on: ubuntu-latest + # Only run build in template repo + if: github.event.repository.name == 'KMP-App-Template' && github.repository_owner == 'Kotlin' + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Desktop debug build + run: ./kotlin build -m desktopApp diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml new file mode 100644 index 0000000..ef00ce0 --- /dev/null +++ b/.github/workflows/build-web.yml @@ -0,0 +1,19 @@ +name: Build web app + +on: + push: + branches: + - kotlin-toolchain + pull_request: + +jobs: + test: + name: Build + runs-on: ubuntu-latest + # Only run build in template repo + if: github.event.repository.name == 'KMP-App-Template' && github.repository_owner == 'Kotlin' + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Web debug build + run: ./kotlin build -m webApp -v debug diff --git a/README.md b/README.md index 437dc50..ee4d18a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![official project](http://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -This is a basic Kotlin Multiplatform app template for Android and iOS. It includes shared business logic and data handling, and a shared UI implementation using Compose Multiplatform. +This is a basic Kotlin Multiplatform app template for Android, iOS, desktop, and web. It includes shared business logic and data handling, and a shared UI implementation using Compose Multiplatform. > The template is also available [with native UI written in Jetpack Compose and SwiftUI](https://github.com/kotlin/KMP-App-Template-Native). diff --git a/androidApp/module.yaml b/androidApp/module.yaml index 8190a26..4aa51aa 100644 --- a/androidApp/module.yaml +++ b/androidApp/module.yaml @@ -7,8 +7,6 @@ dependencies: - $libs.ktor.client.okhttp settings: - kotlin: - version: 2.4.10 # TODO remove once the default catches up compose: enabled: true android: diff --git a/desktopApp/module.yaml b/desktopApp/module.yaml new file mode 100644 index 0000000..f611955 --- /dev/null +++ b/desktopApp/module.yaml @@ -0,0 +1,9 @@ +product: jvm/app + +dependencies: + - ../shared + - $compose.desktop.currentOs + - $libs.kotlinx.coroutines.swing + +settings: + compose: enabled diff --git a/desktopApp/src/main.kt b/desktopApp/src/main.kt new file mode 100644 index 0000000..724687c --- /dev/null +++ b/desktopApp/src/main.kt @@ -0,0 +1,18 @@ +package com.jetbrains.kmpapp + +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.jetbrains.kmpapp.di.initKoin + +fun main() { + initKoin() + + application { + Window( + onCloseRequest = ::exitApplication, + title = "KMP-App-Template", + ) { + App() + } + } +} diff --git a/iosApp/module.xcodeproj/project.pbxproj b/iosApp/module.xcodeproj/project.pbxproj index 866d178..723768e 100644 --- a/iosApp/module.xcodeproj/project.pbxproj +++ b/iosApp/module.xcodeproj/project.pbxproj @@ -22,7 +22,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 39328C78C3709E7A6804F01C /* src */ = { + 39328C78C3709E7A6804F01C /* PBXFileSystemSynchronizedRootGroup */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( 39328B7F9944E62EB90E46E2 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, @@ -53,7 +53,7 @@ children = ( 39328689251DF9513C25544F /* Products */, 393286A8F6039FD41E25C11B /* Info.plist */, - 39328C78C3709E7A6804F01C /* src */, + 39328C78C3709E7A6804F01C /* PBXFileSystemSynchronizedRootGroup */, ); sourceTree = ""; }; @@ -64,7 +64,7 @@ isa = PBXNativeTarget; buildConfigurationList = 393282D7645E8F6DDA49CF5E /* Build configuration list for PBXNativeTarget "app" */; buildPhases = ( - 39328FA843134D3FD996EE1F /* Build Kotlin with Amper */, + 39328FA843134D3FD996EE1F /* Build Kotlin */, 393282358FF7C02959BEF1C2 /* Sources */, 393283130918FF0FFBC2B306 /* Frameworks */, 39328C7D425627F9B3C28542 /* Resources */, @@ -74,7 +74,7 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( - 39328C78C3709E7A6804F01C /* src */, + 39328C78C3709E7A6804F01C /* PBXFileSystemSynchronizedRootGroup */, ); name = app; productName = app; @@ -111,17 +111,17 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 39328FA843134D3FD996EE1F /* Build Kotlin with Amper */ = { + 39328FA843134D3FD996EE1F /* Build Kotlin */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; inputPaths = ( ); - name = "Build Kotlin with Amper"; + name = "Build Kotlin"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# !AMPER KMP INTEGRATION STEP!\n# This script is managed by the Kotlin Toolchain, do not edit manually!\n\"${KOTLIN_CLI_WRAPPER_PATH}\" tool xcode-integration\n"; + shellScript = "# !KOTLIN INTEGRATION STEP!\n# This script is managed by the Kotlin Toolchain, do not edit manually!\n\"${KOTLIN_CLI_WRAPPER_PATH}\" tool xcode-integration\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/iosApp/module.xcodeproj/xcshareddata/xcschemes/app.xcscheme b/iosApp/module.xcodeproj/xcshareddata/xcschemes/app.xcscheme new file mode 100644 index 0000000..8cf5c5b --- /dev/null +++ b/iosApp/module.xcodeproj/xcshareddata/xcschemes/app.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/module.yaml b/iosApp/module.yaml index cca33a8..e164563 100644 --- a/iosApp/module.yaml +++ b/iosApp/module.yaml @@ -6,6 +6,4 @@ dependencies: - ../shared settings: - kotlin: - version: 2.4.10 # TODO remove once the default catches up compose: enabled diff --git a/kotlin b/kotlin index fa7d915..db0dddf 100755 --- a/kotlin +++ b/kotlin @@ -17,9 +17,9 @@ set -e -u # The version of the Kotlin Toolchain (and CLI) distribution to provision and use -kotlin_cli_version=0.11.1 +kotlin_cli_version=0.12.0 # Establish chain of trust from here by specifying exact checksum of Kotlin Toolchain (and CLI) distribution to be run -kotlin_cli_sha256=0ded2a434f6bf193b24e2a6d56c3ba443f4232721155a65aaa8372789412112f +kotlin_cli_sha256=626b2d6e8753ad75b4b02144e597d2f441dd940794be5103352af2e800e4e13e KOTLIN_CLI_DOWNLOAD_ROOT="${KOTLIN_CLI_DOWNLOAD_ROOT:-https://packages.jetbrains.team/maven/p/amper/amper}" diff --git a/kotlin.bat b/kotlin.bat index e7e6b3b..6374243 100755 --- a/kotlin.bat +++ b/kotlin.bat @@ -17,9 +17,9 @@ setlocal @rem The version of the Kotlin Toolchain distribution to provision and use -set kotlin_cli_version=0.11.1 +set kotlin_cli_version=0.12.0 @rem Establish chain of trust from here by specifying the exact checksum of the Kotlin Toolchain distribution to be run -set kotlin_cli_sha256=0ded2a434f6bf193b24e2a6d56c3ba443f4232721155a65aaa8372789412112f +set kotlin_cli_sha256=626b2d6e8753ad75b4b02144e597d2f441dd940794be5103352af2e800e4e13e if not defined KOTLIN_CLI_DOWNLOAD_ROOT set KOTLIN_CLI_DOWNLOAD_ROOT=https://packages.jetbrains.team/maven/p/amper/amper if not defined KOTLIN_CLI_BOOTSTRAP_CACHE_DIR set KOTLIN_CLI_BOOTSTRAP_CACHE_DIR=%LOCALAPPDATA%\JetBrains\Kotlin\cli @@ -175,7 +175,7 @@ if exist "%wrapper_candidate%" ( if exist "%project_dir%\project.yaml" ( @rem Found project.yaml but no wrapper alongside it - echo WARNING: Found a project.yaml in '%project_dir%', but the wrapper script is missing; using Kotlin Toolchain v$kotlin_cli_version. >&2 + echo WARNING: Found a project.yaml in '%project_dir%', but the wrapper script is missing; using Kotlin Toolchain v%kotlin_cli_version%. >&2 exit /b 1 ) @@ -253,5 +253,12 @@ if "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( rem We use busybox here because it doesn't reinterpret the user-passed command-line arguments (that we pass via %*). rem Also this way we can use the unified launcher script (.sh) set KOTLIN_CLI_WRAPPER_PATH=%~f0 -"%busybox_exe%" sh "%kotlin_cli_target_dir%\bin\launcher.sh" %* -exit /B %ERRORLEVEL% + +rem The '& call' after the app run is to avoid the "Terminate batch job (Y/N)?" prompt +"%busybox_exe%" sh "%kotlin_cli_target_dir%\bin\launcher.sh" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem We use "%COMSPEC%" /d /c exit so that and/or operators work properly when calling kotlin.bat directly from a script +@rem without the `call` command. With a plain `exit /B %ERRORLEVEL%`, using `kotlin.bat && echo success` would print +@rem 'success' even in case of failure. Consumers would have to use `call kotlin.bat && echo success` for it to work.` +"%COMSPEC%" /d /c exit %ERRORLEVEL% diff --git a/libs.versions.toml b/libs.versions.toml index 2cefc8c..da7bb72 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -19,6 +19,7 @@ koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", ver ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "compose-material-icons" } diff --git a/project.yaml b/project.yaml index 7a15cb5..e056ef5 100644 --- a/project.yaml +++ b/project.yaml @@ -1,4 +1,6 @@ modules: - androidApp + - desktopApp - iosApp - shared + - webApp diff --git a/shared/module.yaml b/shared/module.yaml index 9fc1a3e..3297254 100644 --- a/shared/module.yaml +++ b/shared/module.yaml @@ -1,6 +1,6 @@ product: type: kmp/lib - platforms: [jvm, android, iosArm64, iosSimulatorArm64] + platforms: [jvm, android, iosArm64, iosSimulatorArm64, wasmJs] dependencies: - $compose.runtime @@ -24,11 +24,13 @@ dependencies@android: dependencies@ios: - $libs.ktor.client.darwin +dependencies@wasmJs: + - $libs.ktor.client.js + settings: compose: enabled: true resources: packageName: kmp_app_template.shared.generated.resources kotlin: - version: 2.4.10 # TODO remove once the default catches up serialization: json diff --git a/shared/src/screens/PlaceholderColor.kt b/shared/src/screens/PlaceholderColor.kt new file mode 100644 index 0000000..352e406 --- /dev/null +++ b/shared/src/screens/PlaceholderColor.kt @@ -0,0 +1,8 @@ +package com.jetbrains.kmpapp.screens + +import androidx.compose.ui.graphics.Color + +fun placeholderColor(objectId: Int): Color { + val hue = (objectId * 137).mod(360) + return Color.hsl(hue.toFloat(), saturation = 0.5f, lightness = 0.75f) +} diff --git a/shared/src/screens/detail/DetailScreen.kt b/shared/src/screens/detail/DetailScreen.kt index 4b72d0b..dc0f028 100644 --- a/shared/src/screens/detail/DetailScreen.kt +++ b/shared/src/screens/detail/DetailScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -37,6 +36,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.jetbrains.kmpapp.data.MuseumObject import com.jetbrains.kmpapp.screens.EmptyScreenContent +import com.jetbrains.kmpapp.screens.placeholderColor import kmp_app_template.shared.generated.resources.Res import kmp_app_template.shared.generated.resources.back import kmp_app_template.shared.generated.resources.label_artist @@ -98,7 +98,7 @@ private fun ObjectDetails( contentScale = ContentScale.FillWidth, modifier = Modifier .fillMaxWidth() - .background(Color.LightGray) + .background(placeholderColor(obj.objectID)) ) SelectionContainer { diff --git a/shared/src/screens/list/ListScreen.kt b/shared/src/screens/list/ListScreen.kt index 4ec94ff..720605b 100644 --- a/shared/src/screens/list/ListScreen.kt +++ b/shared/src/screens/list/ListScreen.kt @@ -21,13 +21,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.jetbrains.kmpapp.data.MuseumObject import com.jetbrains.kmpapp.screens.EmptyScreenContent +import com.jetbrains.kmpapp.screens.placeholderColor import org.koin.compose.viewmodel.koinViewModel @Composable @@ -87,7 +87,7 @@ private fun ObjectFrame( modifier = Modifier .fillMaxWidth() .aspectRatio(1f) - .background(Color.LightGray), + .background(placeholderColor(obj.objectID)), ) Spacer(Modifier.height(2.dp)) diff --git a/webApp/module.yaml b/webApp/module.yaml new file mode 100644 index 0000000..ceb8223 --- /dev/null +++ b/webApp/module.yaml @@ -0,0 +1,10 @@ +product: wasm-js/app + +dependencies: + - ../shared + - $compose.ui + - $libs.coil.compose + - $libs.coil.network.ktor + +settings: + compose: enabled diff --git a/webApp/resources/index.html b/webApp/resources/index.html new file mode 100644 index 0000000..a24359b --- /dev/null +++ b/webApp/resources/index.html @@ -0,0 +1,17 @@ + + + + + + KMP-App-Template + + + + + +
+ Real images not available in the web version due to CORS restrictions of the MET API +
+
+ + diff --git a/webApp/resources/styles.css b/webApp/resources/styles.css new file mode 100644 index 0000000..c27041c --- /dev/null +++ b/webApp/resources/styles.css @@ -0,0 +1,41 @@ +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} + +body { + display: flex; + flex-direction: column; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +#corsNotice { + flex: none; + padding: 8px 16px; + border-bottom: 1px solid #e8d9a0; + background-color: #fff8e1; + color: #4a3b00; + font-size: 14px; + line-height: 1.4; + text-align: center; +} + +#composeTarget { + flex: 1; + min-height: 0; +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #1c1b1f; + } + + #corsNotice { + border-bottom-color: #574c33; + background-color: #3a3222; + color: #f2e2b6; + } +} diff --git a/webApp/src/main.kt b/webApp/src/main.kt new file mode 100644 index 0000000..1db50f0 --- /dev/null +++ b/webApp/src/main.kt @@ -0,0 +1,14 @@ +package com.jetbrains.kmpapp + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import com.jetbrains.kmpapp.di.initKoin + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + initKoin() + + ComposeViewport(viewportContainerId = "composeTarget") { + App() + } +} From 0dc2ae81f0311be2b5424fe2740bd82e16b6fd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Thu, 10 Sep 2026 14:18:12 +0200 Subject: [PATCH 2/3] Update README with app run instructions --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index ee4d18a..1130afa 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,17 @@ This is a basic Kotlin Multiplatform app template for Android, iOS, desktop, and ![Screenshots of the app](images/screenshots.png) +### Running the apps + +Use the run configurations provided by the run widget in your IDE's toolbar (make sure that you have the [Kotlin Multiplatform plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform) installed. + +You can also use these commands and options: + +- Android app: `./kotlin run -m androidApp` +- Desktop app: `./kotlin run -m desktopApp --compose-hot-reload-mode` +- Web app: `./kotlin run -m webApp` +- iOS app: `./kotlin run -m iosApp -p iosSimulatorArm64`, or open the iOS project in Xcode and run it from there. + ### Technologies The data displayed by the app is from [The Metropolitan Museum of Art Collection API](https://metmuseum.github.io/). From a76276c5578b2d6c48748f2b949aa768caa1e3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Thu, 10 Sep 2026 15:02:12 +0200 Subject: [PATCH 3/3] Add okhttp ktor client dependency for JVM --- shared/module.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared/module.yaml b/shared/module.yaml index 3297254..eaad29b 100644 --- a/shared/module.yaml +++ b/shared/module.yaml @@ -18,6 +18,9 @@ dependencies: - $libs.material.icons.core - $libs.navigation.compose +dependencies@jvm: + - $libs.ktor.client.okhttp + dependencies@android: - $libs.ktor.client.okhttp