From 5061e58aa34ac6874766a8354fec3908ca1a1a79 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/2] Add web and desktop support --- .github/workflows/build-desktop.yml | 19 +++++ .github/workflows/build-web.yml | 19 +++++ README.md | 10 ++- androidApp/module.yaml | 12 +-- androidApp/res/values/strings.xml | 13 --- androidApp/src/MainActivity.kt | 46 ----------- androidApp/src/MuseumApp.kt | 12 +-- desktopApp/module.yaml | 9 +++ desktopApp/src/main.kt | 18 +++++ iosApp/module.xcodeproj/project.pbxproj | 14 ++-- .../xcschemes/Module iosApp.xcscheme | 80 +++++++++++++++++++ kotlin | 4 +- kotlin.bat | 17 ++-- libs.versions.toml | 14 ++-- project.yaml | 3 + shared/module.yaml | 8 +- shared/src/di/Koin.kt | 8 ++ sharedUI/composeResources/values/strings.xml | 15 ++++ sharedUI/module.yaml | 25 ++++++ sharedUI/src/App.kt | 47 +++++++++++ .../src/screens/DetailScreen.kt | 31 ++++--- .../src/screens/EmptyScreenContent.kt | 7 +- .../src/screens/ListScreen.kt | 3 +- sharedUI/src/screens/PlaceholderColor.kt | 8 ++ webApp/module.yaml | 10 +++ webApp/resources/index.html | 17 ++++ webApp/resources/styles.css | 41 ++++++++++ webApp/src/main.kt | 14 ++++ 28 files changed, 401 insertions(+), 123 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/Module iosApp.xcscheme mode change 100644 => 100755 kotlin.bat create mode 100644 sharedUI/composeResources/values/strings.xml create mode 100644 sharedUI/module.yaml create mode 100644 sharedUI/src/App.kt rename {androidApp => sharedUI}/src/screens/DetailScreen.kt (73%) rename {androidApp => sharedUI}/src/screens/EmptyScreenContent.kt (61%) rename {androidApp => sharedUI}/src/screens/ListScreen.kt (97%) create mode 100644 sharedUI/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..b3b814c --- /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-Native' && 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..edfee9e --- /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-Native' && 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 78f5938..6a602d4 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 native UI implementations using Jetpack Compose and SwiftUI. +This is a basic Kotlin Multiplatform app template for Android, iOS, desktop, and web. It includes shared business logic and data handling, and native UI implementations using Compose Multiplatform and SwiftUI. > The template is also available [with shared UI written in Compose Multiplatform](https://github.com/kotlin/KMP-App-Template). @@ -22,8 +22,10 @@ The app uses the following multiplatform dependencies in its implementation: > These are just some of the possible libraries to use for these tasks with Kotlin Multiplatform, and their usage here isn't a strong recommendation for these specific libraries over the available alternatives. You can find a wide variety of curated multiplatform libraries in the [kmp-awesome](https://github.com/terrakok/kmp-awesome) repository. -And the following Android-specific dependencies: +The Android, desktop, and web apps share these dependencies: -- [Jetpack Compose](https://developer.android.com/jetpack/compose) -- [Navigation component](https://developer.android.com/jetpack/compose/navigation) +- [Compose Multiplatform](https://jb.gg/compose) for UI +- [Compose Navigation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html) - [Coil](https://github.com/coil-kt/coil) for image loading + +The iOS app is built with [SwiftUI](https://developer.apple.com/xcode/swiftui/). diff --git a/androidApp/module.yaml b/androidApp/module.yaml index 2139b76..ce922e6 100644 --- a/androidApp/module.yaml +++ b/androidApp/module.yaml @@ -1,18 +1,8 @@ product: android/app dependencies: - - ../shared - - $compose.runtime - - $compose.foundation - - $compose.material3 - - $libs.androidx.compose.ui.tooling.preview + - ../sharedUI - $libs.androidx.activity.compose - - $libs.androidx.viewmodel.compose - - $libs.androidx.navigation.compose - - $libs.koin.compose.viewmodel - - $libs.coil.compose - - $libs.coil.network.ktor - - $libs.material.icons.core settings: compose: enabled diff --git a/androidApp/res/values/strings.xml b/androidApp/res/values/strings.xml index ec9f946..22c53f5 100644 --- a/androidApp/res/values/strings.xml +++ b/androidApp/res/values/strings.xml @@ -1,16 +1,3 @@ KMP App - - Title - Artist - Date - Dimensions - Medium - Department - Repository - Credits - - Back - - No data available diff --git a/androidApp/src/MainActivity.kt b/androidApp/src/MainActivity.kt index d51fd72..5c14dc3 100644 --- a/androidApp/src/MainActivity.kt +++ b/androidApp/src/MainActivity.kt @@ -5,19 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import com.jetbrains.kmpapp.screens.DetailScreen -import com.jetbrains.kmpapp.screens.ListScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -32,37 +20,3 @@ class MainActivity : ComponentActivity() { } } } - -@Composable -fun App() { - MaterialTheme( - colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() - ) { - Surface { - NavigationContent() - } - } -} - -@Composable -fun NavigationContent() { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = "list") { - composable("list") { - ListScreen(navigateToDetails = { objectId -> - navController.navigate("details/$objectId") - }) - } - composable( - "details/{objectId}", - arguments = listOf(navArgument("objectId") { type = NavType.IntType }) - ) { backstack -> - DetailScreen( - objectId = backstack.arguments?.getInt("objectId")!!, - navigateBack = { - navController.popBackStack() - } - ) - } - } -} diff --git a/androidApp/src/MuseumApp.kt b/androidApp/src/MuseumApp.kt index 6cf442e..cd4fc5c 100644 --- a/androidApp/src/MuseumApp.kt +++ b/androidApp/src/MuseumApp.kt @@ -2,20 +2,10 @@ package com.jetbrains.kmpapp import android.app.Application import com.jetbrains.kmpapp.di.initKoin -import com.jetbrains.kmpapp.screens.DetailViewModel -import com.jetbrains.kmpapp.screens.ListViewModel -import org.koin.dsl.module class MuseumApp : Application() { override fun onCreate() { super.onCreate() - initKoin( - listOf( - module { - factory { ListViewModel(get()) } - factory { DetailViewModel(get()) } - } - ) - ) + initKoin() } } diff --git a/desktopApp/module.yaml b/desktopApp/module.yaml new file mode 100644 index 0000000..33aee17 --- /dev/null +++ b/desktopApp/module.yaml @@ -0,0 +1,9 @@ +product: jvm/app + +dependencies: + - ../sharedUI + - $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..4afa085 --- /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-Native", + ) { + App() + } + } +} diff --git a/iosApp/module.xcodeproj/project.pbxproj b/iosApp/module.xcodeproj/project.pbxproj index e090227..621a7c2 100644 --- a/iosApp/module.xcodeproj/project.pbxproj +++ b/iosApp/module.xcodeproj/project.pbxproj @@ -22,7 +22,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 3932829DBF51BBABB904748B /* src */ = { + 3932829DBF51BBABB904748B /* PBXFileSystemSynchronizedRootGroup */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( 393283536DE23C7351F8EE5D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, @@ -45,7 +45,7 @@ children = ( 39328DDC0ADDB7D3E94BA030 /* Products */, 3932828801841158B0ECBD49 /* Info.plist */, - 3932829DBF51BBABB904748B /* src */, + 3932829DBF51BBABB904748B /* PBXFileSystemSynchronizedRootGroup */, ); sourceTree = ""; }; @@ -64,7 +64,7 @@ isa = PBXNativeTarget; buildConfigurationList = 393285F919542C808148B5B9 /* Build configuration list for PBXNativeTarget "app" */; buildPhases = ( - 39328688721F461EA052737F /* Build Kotlin with Amper */, + 39328688721F461EA052737F /* Build Kotlin */, 39328B0D6BF0CDA7332EA98C /* Sources */, 39328C22B84BD298BA58714D /* Frameworks */, 393282655F1B9FE38F516DCE /* Resources */, @@ -74,7 +74,7 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( - 3932829DBF51BBABB904748B /* src */, + 3932829DBF51BBABB904748B /* PBXFileSystemSynchronizedRootGroup */, ); name = app; productName = app; @@ -111,17 +111,17 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 39328688721F461EA052737F /* Build Kotlin with Amper */ = { + 39328688721F461EA052737F /* 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/Module iosApp.xcscheme b/iosApp/module.xcodeproj/xcshareddata/xcschemes/Module iosApp.xcscheme new file mode 100644 index 0000000..c62816c --- /dev/null +++ b/iosApp/module.xcodeproj/xcshareddata/xcschemes/Module iosApp.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 old mode 100644 new mode 100755 index e7e6b3b..6374243 --- 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 a0cea77..2cbf239 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -1,27 +1,29 @@ [versions] androidx-activity = "1.13.0" -androidx-compose = "1.10.5" androidx-lifecycle = "2.11.0-beta01" -androidx-navigation = "2.9.8" +androidx-navigation = "2.9.2" coil = "3.5.0" compose-material-icons = "1.7.3" -kmp-observable-viewmodel = "1.0.2" +kmp-observable-viewmodel = "1.1.0" koin = "4.2.2" +kotlinx-coroutines = "1.11.0" ktor = "3.5.1" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } -androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } -androidx-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } kmp-observable-viewmodel = { module = "com.rickclephas.kmp:kmp-observableviewmodel-core", version.ref = "kmp-observable-viewmodel" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 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" } +navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } diff --git a/project.yaml b/project.yaml index 7a15cb5..0b6fb46 100644 --- a/project.yaml +++ b/project.yaml @@ -1,4 +1,7 @@ modules: - androidApp + - desktopApp - iosApp - shared + - sharedUI + - webApp diff --git a/shared/module.yaml b/shared/module.yaml index 380866d..9ea8c3f 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: - $libs.ktor.client.core @@ -9,12 +9,18 @@ dependencies: - $libs.koin.core - $libs.kmp.observable.viewmodel: exported +dependencies@jvm: + - $libs.ktor.client.okhttp + dependencies@android: - $libs.ktor.client.okhttp dependencies@ios: - $libs.ktor.client.darwin +dependencies@wasmJs: + - $libs.ktor.client.js + settings: kotlin: serialization: json diff --git a/shared/src/di/Koin.kt b/shared/src/di/Koin.kt index 32c70a5..d37a394 100644 --- a/shared/src/di/Koin.kt +++ b/shared/src/di/Koin.kt @@ -5,6 +5,8 @@ import com.jetbrains.kmpapp.data.KtorMuseumApi import com.jetbrains.kmpapp.data.MuseumApi import com.jetbrains.kmpapp.data.MuseumRepository import com.jetbrains.kmpapp.data.MuseumStorage +import com.jetbrains.kmpapp.screens.DetailViewModel +import com.jetbrains.kmpapp.screens.ListViewModel import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.http.ContentType @@ -34,12 +36,18 @@ val dataModule = module { } } +val viewModelModule = module { + factory { ListViewModel(get()) } + factory { DetailViewModel(get()) } +} + fun initKoin() = initKoin(emptyList()) fun initKoin(extraModules: List) { startKoin { modules( dataModule, + viewModelModule, *extraModules.toTypedArray(), ) } diff --git a/sharedUI/composeResources/values/strings.xml b/sharedUI/composeResources/values/strings.xml new file mode 100644 index 0000000..849dd42 --- /dev/null +++ b/sharedUI/composeResources/values/strings.xml @@ -0,0 +1,15 @@ + + + Title + Artist + Date + Dimensions + Medium + Department + Repository + Credits + + Back + + No data available + diff --git a/sharedUI/module.yaml b/sharedUI/module.yaml new file mode 100644 index 0000000..3c4388c --- /dev/null +++ b/sharedUI/module.yaml @@ -0,0 +1,25 @@ +product: + type: kmp/lib + platforms: [jvm, android, wasmJs] + +dependencies: + - ../shared: exported + - $compose.runtime + - $compose.foundation + - $compose.material3 + - $compose.components.resources + - $libs.androidx.lifecycle.runtime.compose + - $libs.androidx.lifecycle.viewmodel.compose + - $libs.navigation.compose + - $libs.koin.compose.viewmodel + - $libs.coil.compose + - $libs.coil.network.ktor + - $libs.material.icons.core + +settings: + compose: + enabled: true + resources: + packageName: kmp_app_template_native.sharedui.generated.resources + kotlin: + serialization: json diff --git a/sharedUI/src/App.kt b/sharedUI/src/App.kt new file mode 100644 index 0000000..cec6702 --- /dev/null +++ b/sharedUI/src/App.kt @@ -0,0 +1,47 @@ +package com.jetbrains.kmpapp + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import com.jetbrains.kmpapp.screens.DetailScreen +import com.jetbrains.kmpapp.screens.ListScreen +import kotlinx.serialization.Serializable + +@Serializable +object ListDestination + +@Serializable +data class DetailDestination(val objectId: Int) + +@Composable +fun App() { + MaterialTheme( + colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + ) { + Surface { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = ListDestination) { + composable { + ListScreen(navigateToDetails = { objectId -> + navController.navigate(DetailDestination(objectId)) + }) + } + composable { backStackEntry -> + DetailScreen( + objectId = backStackEntry.toRoute().objectId, + navigateBack = { + navController.popBackStack() + } + ) + } + } + } + } +} diff --git a/androidApp/src/screens/DetailScreen.kt b/sharedUI/src/screens/DetailScreen.kt similarity index 73% rename from androidApp/src/screens/DetailScreen.kt rename to sharedUI/src/screens/DetailScreen.kt index 67c8048..c97ba95 100644 --- a/androidApp/src/screens/DetailScreen.kt +++ b/sharedUI/src/screens/DetailScreen.kt @@ -26,9 +26,7 @@ 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.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -36,8 +34,17 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.jetbrains.kmpapp.R import com.jetbrains.kmpapp.data.MuseumObject +import kmp_app_template_native.sharedui.generated.resources.Res +import kmp_app_template_native.sharedui.generated.resources.back +import kmp_app_template_native.sharedui.generated.resources.label_artist +import kmp_app_template_native.sharedui.generated.resources.label_credits +import kmp_app_template_native.sharedui.generated.resources.label_date +import kmp_app_template_native.sharedui.generated.resources.label_department +import kmp_app_template_native.sharedui.generated.resources.label_dimensions +import kmp_app_template_native.sharedui.generated.resources.label_medium +import kmp_app_template_native.sharedui.generated.resources.label_repository +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @Composable @@ -67,7 +74,7 @@ private fun ObjectDetails( title = {}, navigationIcon = { IconButton(onClick = onBackClick) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) + Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(Res.string.back)) } } ) @@ -85,20 +92,20 @@ private fun ObjectDetails( contentScale = ContentScale.FillWidth, modifier = Modifier .fillMaxWidth() - .background(Color.LightGray) + .background(placeholderColor(obj.objectID)) ) SelectionContainer { Column(Modifier.padding(12.dp)) { Text(obj.title, style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(6.dp)) - LabeledInfo(stringResource(R.string.label_artist), obj.artistDisplayName) - LabeledInfo(stringResource(R.string.label_date), obj.objectDate) - LabeledInfo(stringResource(R.string.label_dimensions), obj.dimensions) - LabeledInfo(stringResource(R.string.label_medium), obj.medium) - LabeledInfo(stringResource(R.string.label_department), obj.department) - LabeledInfo(stringResource(R.string.label_repository), obj.repository) - LabeledInfo(stringResource(R.string.label_credits), obj.creditLine) + LabeledInfo(stringResource(Res.string.label_artist), obj.artistDisplayName) + LabeledInfo(stringResource(Res.string.label_date), obj.objectDate) + LabeledInfo(stringResource(Res.string.label_dimensions), obj.dimensions) + LabeledInfo(stringResource(Res.string.label_medium), obj.medium) + LabeledInfo(stringResource(Res.string.label_department), obj.department) + LabeledInfo(stringResource(Res.string.label_repository), obj.repository) + LabeledInfo(stringResource(Res.string.label_credits), obj.creditLine) } } } diff --git a/androidApp/src/screens/EmptyScreenContent.kt b/sharedUI/src/screens/EmptyScreenContent.kt similarity index 61% rename from androidApp/src/screens/EmptyScreenContent.kt rename to sharedUI/src/screens/EmptyScreenContent.kt index cdb52cb..2852ffc 100644 --- a/androidApp/src/screens/EmptyScreenContent.kt +++ b/sharedUI/src/screens/EmptyScreenContent.kt @@ -5,8 +5,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.jetbrains.kmpapp.R +import kmp_app_template_native.sharedui.generated.resources.Res +import kmp_app_template_native.sharedui.generated.resources.no_data_available +import org.jetbrains.compose.resources.stringResource @Composable fun EmptyScreenContent( @@ -16,6 +17,6 @@ fun EmptyScreenContent( modifier = modifier, contentAlignment = Alignment.Center, ) { - Text(stringResource(R.string.no_data_available)) + Text(stringResource(Res.string.no_data_available)) } } diff --git a/androidApp/src/screens/ListScreen.kt b/sharedUI/src/screens/ListScreen.kt similarity index 97% rename from androidApp/src/screens/ListScreen.kt rename to sharedUI/src/screens/ListScreen.kt index 354d9cc..88f8d6f 100644 --- a/androidApp/src/screens/ListScreen.kt +++ b/sharedUI/src/screens/ListScreen.kt @@ -23,7 +23,6 @@ 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 @@ -89,7 +88,7 @@ private fun ObjectFrame( modifier = Modifier .fillMaxWidth() .aspectRatio(1f) - .background(Color.LightGray), + .background(placeholderColor(obj.objectID)), ) Spacer(Modifier.height(2.dp)) diff --git a/sharedUI/src/screens/PlaceholderColor.kt b/sharedUI/src/screens/PlaceholderColor.kt new file mode 100644 index 0000000..352e406 --- /dev/null +++ b/sharedUI/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/webApp/module.yaml b/webApp/module.yaml new file mode 100644 index 0000000..1872399 --- /dev/null +++ b/webApp/module.yaml @@ -0,0 +1,10 @@ +product: wasm-js/app + +dependencies: + - ../sharedUI + - $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..ec5ee57 --- /dev/null +++ b/webApp/resources/index.html @@ -0,0 +1,17 @@ + + + + + + KMP-App-Template-Native + + + + + +
+ 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 c1ed1f0bb73c017597b2f6bf57f86d9ec2d2b5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Thu, 10 Sep 2026 14:18:09 +0200 Subject: [PATCH 2/2] Update README with app run instructions --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 6a602d4..d520a0c 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/).