From 9a852dee9a0fb8ec61ea8b637a402925f87fcb73 Mon Sep 17 00:00:00 2001 From: Jakub Jasinski Date: Fri, 29 May 2026 14:01:41 +0200 Subject: [PATCH 1/5] KMP-1: add Compose Multiplatform and library-kmp-module convention plugin Introduces a `library-kmp-module` convention plugin in buildSrc so modules can target Android and iOS with Compose Multiplatform. Uses the AGP `com.android.kotlin.multiplatform.library` plugin (avoids the kotlin.android <-> kotlin.multiplatform conflict) and gates iOS targets to macOS. --- buildSrc/build.gradle.kts | 1 + .../main/kotlin/library-kmp-module.gradle.kts | 51 +++++++++++++++++++ .../src/main/kotlin/util/PluginAccessors.kt | 3 ++ config/libs.toml | 4 ++ 4 files changed, 59 insertions(+) create mode 100644 buildSrc/src/main/kotlin/library-kmp-module.gradle.kts diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 4d2eddc2..e3defcad 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.kotlin.plugin) implementation(libs.kotlin.plugin.compose) implementation(libs.kotlin.plugin.serialization) + implementation(libs.compose.gradlePlugin) implementation(libs.kotlinova.gradle) implementation(libs.metro.plugin) implementation(libs.moduleGraphAssert) diff --git a/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts b/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts new file mode 100644 index 00000000..f192bcd0 --- /dev/null +++ b/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts @@ -0,0 +1,51 @@ +import dev.detekt.gradle.Detekt +import tasks.setupTooManyKotlinFilesTaskForCommon + +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("com.android.kotlin.multiplatform.library") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.compose") + + id("checks") +} + +val isMac = System.getProperty("os.name").contains("mac", ignoreCase = true) + +kotlin { + jvmToolchain(21) + + androidLibrary { + // Modules with resources must override this. + namespace = "com.matejdro.micropebble.noresources" + compileSdk = 36 + minSdk = 30 + } + + if (isMac) { + iosX64() + iosArm64() + iosSimulatorArm64() + } + + compilerOptions { + optIn.add("kotlin.time.ExperimentalTime") + optIn.add("kotlin.uuid.ExperimentalUuidApi") + optIn.add("kotlin.ExperimentalUnsignedTypes") + freeCompilerArgs.add("-Xannotation-default-target=param-property") + } + + sourceSets { + commonMain.dependencies { + implementation(compose.runtime) + } + } +} + +if (name.startsWith("common-")) { + setupTooManyKotlinFilesTaskForCommon() +} + +tasks.register("runDebugDetekt") { + dependsOn(tasks.withType()) +} diff --git a/buildSrc/src/main/kotlin/util/PluginAccessors.kt b/buildSrc/src/main/kotlin/util/PluginAccessors.kt index f27c80ef..fdd6d424 100644 --- a/buildSrc/src/main/kotlin/util/PluginAccessors.kt +++ b/buildSrc/src/main/kotlin/util/PluginAccessors.kt @@ -16,6 +16,9 @@ inline val PluginDependenciesSpec.androidAppModule: PluginDependencySpec inline val PluginDependenciesSpec.androidLibraryModule: PluginDependencySpec get() = id("library-android-module") +inline val PluginDependenciesSpec.kmpLibraryModule: PluginDependencySpec + get() = id("library-kmp-module") + inline val PluginDependenciesSpec.pureKotlinModule: PluginDependencySpec get() = id("pure-kotlin-module") diff --git a/config/libs.toml b/config/libs.toml index d8d33910..6b15c39a 100644 --- a/config/libs.toml +++ b/config/libs.toml @@ -21,6 +21,8 @@ androidx-test-runner = "1.7.0" androidx-work = "2.11.2" coil = "3.4.0" composeDnd = "0.4.0" +# @pin needs to match libpebble3's Compose Multiplatform version +composeMultiplatform = "1.10.1" composePreference = "2.2.0" composeWebview = "0.33.6" dependencyAnalysis = "3.15.0" @@ -102,6 +104,8 @@ androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } composeDnd = { module = "com.mohamedrejeb.dnd:compose-dnd", version.ref = "composeDnd" } +# Compose Multiplatform Gradle plugin, used by the library-kmp-module convention plugin in buildSrc +compose-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeMultiplatform" } composePreference = { module = "me.zhanghai.compose.preference:preference", version.ref = "composePreference" } composeWebview = { module = "io.github.kevinnzou:compose-webview", version.ref = "composeWebview" } dependencyAnalysis = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } From 6727e73f9aff48fceafb2a41c99efd4db0ded44a Mon Sep 17 00:00:00 2001 From: Jakub Jasinski Date: Fri, 29 May 2026 14:15:42 +0200 Subject: [PATCH 2/5] KMP-2: make common-navigation compile on Android and iOS Converts common-navigation to a Kotlin Multiplatform module via the library-kmp-module convention. --- .../main/kotlin/library-kmp-module.gradle.kts | 5 +++- common-navigation/build.gradle.kts | 27 ++++++++++++------- .../PredictiveBackFadeAnimationSpec.kt | 0 .../instructions/ReplaceTabContentWith.kt | 0 .../navigation/keys/AppstoreScreenKey.kt | 0 .../keys/AppstoreSourcesScreenKey.kt | 0 .../navigation/keys/BluetoothScanScreenKey.kt | 0 .../navigation/keys/CalendarListScreenKey.kt | 0 .../keys/DeveloperConnectionScreenKey.kt | 0 .../keys/FirmwareUpdateScreenKey.kt | 0 .../navigation/keys/HomeScreenKey.kt | 0 .../navigation/keys/NotificationAppListKey.kt | 0 .../navigation/keys/OnboardingKey.kt | 0 .../navigation/keys/WatchListKey.kt | 0 .../navigation/keys/WatchSettingsScreenKey.kt | 0 .../navigation/keys/WatchappListKey.kt | 0 .../keys/WebservicesAuthScreenKey.kt | 0 .../navigation/keys/base/BaseScreenKey.kt | 0 .../navigation/keys/base/TabContainerKey.kt | 0 .../navigation/keys/common/InputFile.kt | 0 config/libs.toml | 4 +++ 21 files changed, 25 insertions(+), 11 deletions(-) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt (100%) rename common-navigation/src/{main => androidMain}/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt (100%) diff --git a/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts b/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts index f192bcd0..12076b88 100644 --- a/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts +++ b/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts @@ -1,6 +1,9 @@ import dev.detekt.gradle.Detekt +import org.gradle.accessors.dm.LibrariesForLibs import tasks.setupTooManyKotlinFilesTaskForCommon +val libs = the() + plugins { id("org.jetbrains.kotlin.multiplatform") id("com.android.kotlin.multiplatform.library") @@ -37,7 +40,7 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(compose.runtime) + implementation(libs.compose.runtime) } } } diff --git a/common-navigation/build.gradle.kts b/common-navigation/build.gradle.kts index 2062b4e8..0e37260e 100644 --- a/common-navigation/build.gradle.kts +++ b/common-navigation/build.gradle.kts @@ -1,14 +1,21 @@ plugins { - androidLibraryModule - compose - parcelize - serialization + kmpLibraryModule + id("org.jetbrains.kotlin.plugin.serialization") } -dependencies { - api(libs.kotlinova.navigation) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.core) - implementation(libs.androidx.compose.material3.sizeClasses) - implementation(libs.kotlinova.compose) +kotlin { + androidLibrary { + namespace = "com.matejdro.micropebble.navigation" + } + + sourceSets { + androidMain.dependencies { + api(libs.kotlinova.navigation) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core) + implementation(libs.kotlin.serialization.core) + implementation(libs.compose.animation) + implementation(libs.compose.ui) + } + } } diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt diff --git a/config/libs.toml b/config/libs.toml index 6b15c39a..3526aaac 100644 --- a/config/libs.toml +++ b/config/libs.toml @@ -104,8 +104,12 @@ androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } composeDnd = { module = "com.mohamedrejeb.dnd:compose-dnd", version.ref = "composeDnd" } +# Compose Multiplatform artifacts (used by KMP modules; android variants relocate to androidx.compose) +compose-animation = { module = "org.jetbrains.compose.animation:animation", version.ref = "composeMultiplatform" } # Compose Multiplatform Gradle plugin, used by the library-kmp-module convention plugin in buildSrc compose-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeMultiplatform" } +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } composePreference = { module = "me.zhanghai.compose.preference:preference", version.ref = "composePreference" } composeWebview = { module = "io.github.kevinnzou:compose-webview", version.ref = "composeWebview" } dependencyAnalysis = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } From 62354144d16a1cae4ea5ade47e552fd55ac276a5 Mon Sep 17 00:00:00 2001 From: Jakub Jasinski Date: Sat, 30 May 2026 09:21:06 +0200 Subject: [PATCH 3/5] KMP-3: introduce shared Tab model and adapt SelectedTabContent --- .../navigation/keys/NotificationAppListKey.kt | 6 +- .../navigation/keys/WatchListKey.kt | 6 +- .../navigation/keys/WatchappListKey.kt | 6 +- .../navigation/keys/base/TabContainerKey.kt | 8 --- .../micropebble/navigation/keys/base/Tab.kt | 21 ++++++ .../matejdro/micropebble/home/HomeScreen.kt | 64 +++++++++++-------- .../micropebble/tools/ToolsScreenKey.kt | 6 +- .../navigation/scenes/TabListScene.kt | 3 +- 8 files changed, 79 insertions(+), 41 deletions(-) create mode 100644 common-navigation/src/commonMain/kotlin/com/matejdro/micropebble/navigation/keys/base/Tab.kt diff --git a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt index 9643d748..c333f2c4 100644 --- a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt @@ -1,7 +1,11 @@ package com.matejdro.micropebble.navigation.keys +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey import kotlinx.serialization.Serializable import si.inova.kotlinova.navigation.screenkeys.ScreenKey @Serializable -data object NotificationAppListKey : ScreenKey() +data object NotificationAppListKey : ScreenKey(), TabKey { + override val tab get() = Tab.NOTIFICATIONS +} diff --git a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt index 12f699a7..ae047f5f 100644 --- a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt @@ -1,7 +1,11 @@ package com.matejdro.micropebble.navigation.keys +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey import kotlinx.serialization.Serializable import si.inova.kotlinova.navigation.screenkeys.ScreenKey @Serializable -data object WatchListKey : ScreenKey() +data object WatchListKey : ScreenKey(), TabKey { + override val tab get() = Tab.WATCHES +} diff --git a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt index f0c66507..03ad4144 100644 --- a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt @@ -1,5 +1,7 @@ package com.matejdro.micropebble.navigation.keys +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey import com.matejdro.micropebble.navigation.keys.common.InputFile import kotlinx.serialization.Serializable import si.inova.kotlinova.navigation.screenkeys.ScreenKey @@ -7,7 +9,9 @@ import si.inova.kotlinova.navigation.screenkeys.ScreenKey @Serializable data class WatchappListKey( val pbwFile: InputFile? = null, -) : ScreenKey() { +) : ScreenKey(), TabKey { + override val tab get() = Tab.WATCH_APPS + override fun getScopeTag(): String { return this::class.java.name } diff --git a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt index 693be87c..426871af 100644 --- a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt @@ -1,14 +1,6 @@ package com.matejdro.micropebble.navigation.keys.base -import androidx.compose.runtime.Composable -import androidx.compose.runtime.staticCompositionLocalOf -import si.inova.kotlinova.navigation.screenkeys.ScreenKey - /** * Key for a screen that shows a list of tabs + container. Container will be filled from the next entry on the backstack. */ interface TabContainerKey - -val LocalSelectedTabContent = staticCompositionLocalOf { error("SelectedTabContent not provided") } - -data class SelectedTabContent(val content: @Composable () -> Unit, val key: ScreenKey, val contentKey: Any) diff --git a/common-navigation/src/commonMain/kotlin/com/matejdro/micropebble/navigation/keys/base/Tab.kt b/common-navigation/src/commonMain/kotlin/com/matejdro/micropebble/navigation/keys/base/Tab.kt new file mode 100644 index 00000000..19e83823 --- /dev/null +++ b/common-navigation/src/commonMain/kotlin/com/matejdro/micropebble/navigation/keys/base/Tab.kt @@ -0,0 +1,21 @@ +package com.matejdro.micropebble.navigation.keys.base + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf + +enum class Tab { + WATCHES, + WATCH_APPS, + NOTIFICATIONS, + TOOLS, +} + +/** Implemented by screen keys that act as a top-level tab destination. */ +interface TabKey { + val tab: Tab +} + +val LocalSelectedTabContent = staticCompositionLocalOf { error("SelectedTabContent not provided") } + +/** Content shown inside a tab container, plus which [Tab] is currently selected (null when none is). */ +data class SelectedTabContent(val content: @Composable () -> Unit, val tab: Tab?, val contentKey: Any) diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt b/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt index 39f5fd30..8ef765ef 100644 --- a/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt +++ b/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt @@ -38,6 +38,7 @@ import com.matejdro.micropebble.navigation.keys.WatchListKey import com.matejdro.micropebble.navigation.keys.WatchappListKey import com.matejdro.micropebble.navigation.keys.base.LocalSelectedTabContent import com.matejdro.micropebble.navigation.keys.base.SelectedTabContent +import com.matejdro.micropebble.navigation.keys.base.Tab import com.matejdro.micropebble.tools.ToolsScreenKey import com.matejdro.micropebble.ui.debugging.FullScreenPreviews import com.matejdro.micropebble.ui.debugging.PreviewTheme @@ -60,17 +61,24 @@ class HomeScreen( HomeScreenContent( LocalSelectedTabContent.current, tabletMode = sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded, - switchScreen = { navigator.navigate(ReplaceTabContentWith(it)) }, + switchTab = { navigator.navigate(ReplaceTabContentWith(it.toScreenKey())) }, ) } } } +private fun Tab.toScreenKey(): ScreenKey = when (this) { + Tab.WATCHES -> WatchListKey + Tab.WATCH_APPS -> WatchappListKey() + Tab.NOTIFICATIONS -> NotificationAppListKey + Tab.TOOLS -> ToolsScreenKey +} + @Composable private fun HomeScreenContent( selectedTab: SelectedTabContent, tabletMode: Boolean, - switchScreen: (ScreenKey) -> Unit, + switchTab: (Tab) -> Unit, ) { val animatedMainContent: @Composable () -> Unit = { AnimatedContent( @@ -82,17 +90,17 @@ private fun HomeScreenContent( } } if (tabletMode) { - NavigationRailContent(animatedMainContent, selectedTab.key, switchScreen) + NavigationRailContent(animatedMainContent, selectedTab.tab, switchTab) } else { - NavigationBarContent(animatedMainContent, selectedTab.key, switchScreen) + NavigationBarContent(animatedMainContent, selectedTab.tab, switchTab) } } @Composable private fun NavigationBarContent( mainContent: @Composable () -> Unit, - selectedScreen: ScreenKey, - switchScreen: (ScreenKey) -> Unit, + selectedTab: Tab?, + switchTab: (Tab) -> Unit, ) { Column { Box( @@ -105,29 +113,29 @@ private fun NavigationBarContent( NavigationBar { NavigationBarItem( - selected = selectedScreen is WatchListKey, - onClick = { switchScreen(WatchListKey) }, + selected = selectedTab == Tab.WATCHES, + onClick = { switchTab(Tab.WATCHES) }, icon = { Icon(painter = painterResource(id = R.drawable.watches), contentDescription = null) }, label = { Text(stringResource(R.string.watches)) } ) NavigationBarItem( - selected = selectedScreen is WatchappListKey, - onClick = { switchScreen(WatchappListKey()) }, + selected = selectedTab == Tab.WATCH_APPS, + onClick = { switchTab(Tab.WATCH_APPS) }, icon = { Icon(painter = painterResource(id = R.drawable.watchapps), contentDescription = null) }, label = { Text(stringResource(R.string.watch_apps)) } ) NavigationBarItem( - selected = selectedScreen is NotificationAppListKey, - onClick = { switchScreen(NotificationAppListKey) }, + selected = selectedTab == Tab.NOTIFICATIONS, + onClick = { switchTab(Tab.NOTIFICATIONS) }, icon = { Icon(painter = painterResource(id = R.drawable.notifications), contentDescription = null) }, label = { Text(stringResource(R.string.notifications)) } ) NavigationBarItem( - selected = selectedScreen is ToolsScreenKey, - onClick = { switchScreen(ToolsScreenKey) }, + selected = selectedTab == Tab.TOOLS, + onClick = { switchTab(Tab.TOOLS) }, icon = { Icon(painter = painterResource(id = R.drawable.tools), contentDescription = null) }, label = { Text(stringResource(R.string.tools)) } ) @@ -138,35 +146,35 @@ private fun NavigationBarContent( @Composable private fun NavigationRailContent( mainContent: @Composable () -> Unit, - selectedScreen: ScreenKey, - switchScreen: (ScreenKey) -> Unit, + selectedTab: Tab?, + switchTab: (Tab) -> Unit, ) { Row { NavigationRail { NavigationRailItem( - selected = selectedScreen is WatchListKey, - onClick = { switchScreen(WatchListKey) }, + selected = selectedTab == Tab.WATCHES, + onClick = { switchTab(Tab.WATCHES) }, icon = { Icon(painter = painterResource(id = R.drawable.watches), contentDescription = null) }, label = { Text(stringResource(R.string.watches)) } ) NavigationRailItem( - selected = selectedScreen is WatchappListKey, - onClick = { switchScreen(WatchappListKey()) }, + selected = selectedTab == Tab.WATCH_APPS, + onClick = { switchTab(Tab.WATCH_APPS) }, icon = { Icon(painter = painterResource(id = R.drawable.watchapps), contentDescription = null) }, label = { Text(stringResource(R.string.watch_apps)) } ) NavigationRailItem( - selected = selectedScreen is NotificationAppListKey, - onClick = { switchScreen(NotificationAppListKey) }, + selected = selectedTab == Tab.NOTIFICATIONS, + onClick = { switchTab(Tab.NOTIFICATIONS) }, icon = { Icon(painter = painterResource(id = R.drawable.notifications), contentDescription = null) }, label = { Text(stringResource(R.string.notifications)) } ) NavigationRailItem( - selected = selectedScreen is ToolsScreenKey, - onClick = { switchScreen(ToolsScreenKey) }, + selected = selectedTab == Tab.TOOLS, + onClick = { switchTab(Tab.TOOLS) }, icon = { Icon(painter = painterResource(id = R.drawable.tools), contentDescription = null) }, label = { Text(stringResource(R.string.tools)) } ) @@ -197,10 +205,10 @@ internal fun HomePhonePreview() { .background(Color.Red) ) }, - WatchappListKey(), + Tab.WATCH_APPS, "" ), - switchScreen = {}, + switchTab = {}, ) } } @@ -220,10 +228,10 @@ internal fun HomeTabletPreview() { .background(Color.Red) ) }, - WatchappListKey(), + Tab.WATCH_APPS, "" ), - switchScreen = {}, + switchTab = {}, ) } } diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt b/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt index af47c378..77b2713c 100644 --- a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt +++ b/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt @@ -1,7 +1,11 @@ package com.matejdro.micropebble.tools +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey import kotlinx.serialization.Serializable import si.inova.kotlinova.navigation.screenkeys.ScreenKey @Serializable -data object ToolsScreenKey : ScreenKey() +data object ToolsScreenKey : ScreenKey(), TabKey { + override val tab get() = Tab.TOOLS +} diff --git a/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt b/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt index 283bc5d5..5454db73 100644 --- a/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt +++ b/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt @@ -10,6 +10,7 @@ import androidx.navigation3.scene.SceneStrategyScope import com.matejdro.micropebble.navigation.keys.base.LocalSelectedTabContent import com.matejdro.micropebble.navigation.keys.base.SelectedTabContent import com.matejdro.micropebble.navigation.keys.base.TabContainerKey +import com.matejdro.micropebble.navigation.keys.base.TabKey import si.inova.kotlinova.navigation.navigation3.key import si.inova.kotlinova.navigation.screenkeys.ScreenKey @@ -24,7 +25,7 @@ class TabListScene( val selectedTabContent = SelectedTabContent( displayedEntry::Content, - displayedEntry.key(), + (displayedEntry.key() as? TabKey)?.tab, contentKey = displayedEntry.contentKey ) From f39f7b692d714005930ca7ff69c3d0e0fdea45fc Mon Sep 17 00:00:00 2001 From: Jakub Jasinski Date: Mon, 15 Jun 2026 10:34:42 +0200 Subject: [PATCH 4/5] KMP-4: make home/ui compile on Android and iOS Converts home/ui to a Kotlin Multiplatform module. All existing code moves to the Android source set unchanged, and the shared source set is left empty, so iOS compiles with nothing in it yet. - switch home/ui to the multiplatform module convention - move all sources and resources into androidMain, with no code changes - apply the Metro, KSP and serialization plugins directly - run showkase and the navigation compiler through kspAndroid - keep the Accompanist permissions opt-in on the Android source set only - drop the unused parcelize plugin --- home/ui/build.gradle.kts | 81 +++++++++++++------ .../matejdro/micropebble/home/HomeScreen.kt | 0 .../onboarding/OnboardingScreen.kt | 0 .../onboarding/OnboardingScreenViewModel.kt | 0 .../matejdro/micropebble/tools/ToolsScreen.kt | 0 .../micropebble/tools/ToolsScreenKey.kt | 0 .../micropebble/tools/ToolsViewModel.kt | 0 .../watchsettings/WatchSettingsScreen.kt | 0 .../watchsettings/WatchSettingsViewModel.kt | 0 .../res/drawable/appstore_sources.xml | 0 .../res/drawable/calendar_settings.xml | 0 .../res/drawable/developer_connection.xml | 0 .../res/drawable/logs.xml | 0 .../res/drawable/notifications.xml | 0 .../res/drawable/permissions.xml | 0 .../res/drawable/tools.xml | 0 .../res/drawable/watchapps.xml | 0 .../res/drawable/watches.xml | 0 .../res/drawable/webservices_auth.xml | 0 .../res/values/strings.xml | 0 .../src/main/res/values/strings.xml | 6 +- 21 files changed, 59 insertions(+), 28 deletions(-) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/home/HomeScreen.kt (100%) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt (100%) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt (100%) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt (100%) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt (100%) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt (100%) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt (100%) rename home/ui/src/{main => androidMain}/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt (100%) rename home/ui/src/{main => androidMain}/res/drawable/appstore_sources.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/calendar_settings.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/developer_connection.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/logs.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/notifications.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/permissions.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/tools.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/watchapps.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/watches.xml (100%) rename home/ui/src/{main => androidMain}/res/drawable/webservices_auth.xml (100%) rename home/ui/src/{main => androidMain}/res/values/strings.xml (100%) diff --git a/home/ui/build.gradle.kts b/home/ui/build.gradle.kts index 92d83750..f4bddd59 100644 --- a/home/ui/build.gradle.kts +++ b/home/ui/build.gradle.kts @@ -1,33 +1,62 @@ plugins { - androidLibraryModule - compose - di - navigation - parcelize - serialization - showkase + kmpLibraryModule + id("dev.zacsweers.metro") + id("com.google.devtools.ksp") + id("org.jetbrains.kotlin.plugin.serialization") } -android { - namespace = "com.matejdro.micropebble.home.ui" - androidResources.enable = true +kotlin { + androidLibrary { + namespace = "com.matejdro.micropebble.home.ui" + androidResources.enable = true + } + + sourceSets { + androidMain { + languageSettings.optIn("com.google.accompanist.permissions.ExperimentalPermissionsApi") + + dependencies { + api(projects.notification.api) + api(projects.common) + api(projects.logging.api) + api(projects.voice.api) + api(libs.androidx.core) + api(libs.kotlinova.core) + api(libs.kotlinova.navigation) + + implementation(projects.commonNavigation) + implementation(projects.sharedResources) + implementation(projects.commonCompose) + implementation(libs.accompanist.permissions) + implementation(libs.composePreference) + implementation(libs.kotlin.coroutines) + implementation(libs.kotlin.serialization.core) + implementation(libs.dispatch) + implementation(libs.androidx.compose.material3.sizeClasses) + implementation(libs.libpebble3) + implementation(libs.showkase) + + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.lifecycle.compose) + implementation(libs.kotlinova.compose) + } + } + } +} + +val stableClassesFile = project.layout.settingsDirectory.file("config/global_compose_stable_classes.txt") +composeCompiler { + stabilityConfigurationFiles.add(stableClassesFile) +} + +ksp { + arg("skipPrivatePreviews", "true") } dependencies { - api(projects.notification.api) - api(projects.common) - api(projects.logging.api) - api(projects.voice.api) - api(libs.androidx.core) - api(libs.kotlinova.core) - api(libs.kotlinova.navigation) - - implementation(projects.sharedResources) - implementation(projects.commonCompose) - implementation(libs.accompanist.permissions) - implementation(libs.composePreference) - implementation(libs.kotlin.coroutines) - implementation(libs.dispatch) - implementation(libs.androidx.compose.material3.sizeClasses) - implementation(libs.libpebble3) + add("kspAndroid", libs.showkase.processor) + add("kspAndroid", libs.kotlinova.navigation.compiler) } diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt diff --git a/home/ui/src/main/res/drawable/appstore_sources.xml b/home/ui/src/androidMain/res/drawable/appstore_sources.xml similarity index 100% rename from home/ui/src/main/res/drawable/appstore_sources.xml rename to home/ui/src/androidMain/res/drawable/appstore_sources.xml diff --git a/home/ui/src/main/res/drawable/calendar_settings.xml b/home/ui/src/androidMain/res/drawable/calendar_settings.xml similarity index 100% rename from home/ui/src/main/res/drawable/calendar_settings.xml rename to home/ui/src/androidMain/res/drawable/calendar_settings.xml diff --git a/home/ui/src/main/res/drawable/developer_connection.xml b/home/ui/src/androidMain/res/drawable/developer_connection.xml similarity index 100% rename from home/ui/src/main/res/drawable/developer_connection.xml rename to home/ui/src/androidMain/res/drawable/developer_connection.xml diff --git a/home/ui/src/main/res/drawable/logs.xml b/home/ui/src/androidMain/res/drawable/logs.xml similarity index 100% rename from home/ui/src/main/res/drawable/logs.xml rename to home/ui/src/androidMain/res/drawable/logs.xml diff --git a/home/ui/src/main/res/drawable/notifications.xml b/home/ui/src/androidMain/res/drawable/notifications.xml similarity index 100% rename from home/ui/src/main/res/drawable/notifications.xml rename to home/ui/src/androidMain/res/drawable/notifications.xml diff --git a/home/ui/src/main/res/drawable/permissions.xml b/home/ui/src/androidMain/res/drawable/permissions.xml similarity index 100% rename from home/ui/src/main/res/drawable/permissions.xml rename to home/ui/src/androidMain/res/drawable/permissions.xml diff --git a/home/ui/src/main/res/drawable/tools.xml b/home/ui/src/androidMain/res/drawable/tools.xml similarity index 100% rename from home/ui/src/main/res/drawable/tools.xml rename to home/ui/src/androidMain/res/drawable/tools.xml diff --git a/home/ui/src/main/res/drawable/watchapps.xml b/home/ui/src/androidMain/res/drawable/watchapps.xml similarity index 100% rename from home/ui/src/main/res/drawable/watchapps.xml rename to home/ui/src/androidMain/res/drawable/watchapps.xml diff --git a/home/ui/src/main/res/drawable/watches.xml b/home/ui/src/androidMain/res/drawable/watches.xml similarity index 100% rename from home/ui/src/main/res/drawable/watches.xml rename to home/ui/src/androidMain/res/drawable/watches.xml diff --git a/home/ui/src/main/res/drawable/webservices_auth.xml b/home/ui/src/androidMain/res/drawable/webservices_auth.xml similarity index 100% rename from home/ui/src/main/res/drawable/webservices_auth.xml rename to home/ui/src/androidMain/res/drawable/webservices_auth.xml diff --git a/home/ui/src/main/res/values/strings.xml b/home/ui/src/androidMain/res/values/strings.xml similarity index 100% rename from home/ui/src/main/res/values/strings.xml rename to home/ui/src/androidMain/res/values/strings.xml diff --git a/shared-resources/src/main/res/values/strings.xml b/shared-resources/src/main/res/values/strings.xml index f4a20e36..044e9ba5 100644 --- a/shared-resources/src/main/res/values/strings.xml +++ b/shared-resources/src/main/res/values/strings.xml @@ -1,11 +1,13 @@ - + Connecting Micro Pebble OK Cancel Appstore sources - Webservices + + Webservices Pebble Classic Pebble Steel From f78f042aa5c90f7147d1f244b9d1b0c7098ac619 Mon Sep 17 00:00:00 2001 From: Jakub Jasinski Date: Mon, 1 Jun 2026 10:41:06 +0200 Subject: [PATCH 5/5] KMP-5: move HomeScreenContent into shared code Moves the home screen's content and its navigation bar/rail into the shared source set so the home screen's visuals run on both Android and iOS. - move HomeScreenContent and the nav bar/rail into commonMain - make it data-driven over a list of tabs (tab + icon + label) so it no longer depends on Android resources - build that tab list from Android resources in the Android HomeScreen wrapper - keep the kotlinova screen wrapper, window-size check and previews on Android - add Compose Multiplatform material3 and foundation to commonMain --- config/libs.toml | 4 + home/ui/build.gradle.kts | 9 +- .../matejdro/micropebble/home/HomeScreen.kt | 168 ++++-------------- .../micropebble/home/HomeScreenContent.kt | 102 +++++++++++ .../com/matejdro/micropebble/home/HomeTab.kt | 6 + 5 files changed, 153 insertions(+), 136 deletions(-) create mode 100644 home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeScreenContent.kt create mode 100644 home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeTab.kt diff --git a/config/libs.toml b/config/libs.toml index 3526aaac..de229bbc 100644 --- a/config/libs.toml +++ b/config/libs.toml @@ -21,6 +21,8 @@ androidx-test-runner = "1.7.0" androidx-work = "2.11.2" coil = "3.4.0" composeDnd = "0.4.0" +# @pin CMP material3 tracks its own version; 1.10.0-alpha05 matches libpebble3 (1.11.x alpha is broken) +composeMaterial3 = "1.10.0-alpha05" # @pin needs to match libpebble3's Compose Multiplatform version composeMultiplatform = "1.10.1" composePreference = "2.2.0" @@ -106,8 +108,10 @@ coil-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = " composeDnd = { module = "com.mohamedrejeb.dnd:compose-dnd", version.ref = "composeDnd" } # Compose Multiplatform artifacts (used by KMP modules; android variants relocate to androidx.compose) compose-animation = { module = "org.jetbrains.compose.animation:animation", version.ref = "composeMultiplatform" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } # Compose Multiplatform Gradle plugin, used by the library-kmp-module convention plugin in buildSrc compose-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeMultiplatform" } +compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMaterial3" } compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } composePreference = { module = "me.zhanghai.compose.preference:preference", version.ref = "composePreference" } diff --git a/home/ui/build.gradle.kts b/home/ui/build.gradle.kts index f4bddd59..26ff5a89 100644 --- a/home/ui/build.gradle.kts +++ b/home/ui/build.gradle.kts @@ -12,6 +12,14 @@ kotlin { } sourceSets { + commonMain.dependencies { + implementation(projects.commonNavigation) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.animation) + implementation(libs.compose.ui) + } + androidMain { languageSettings.optIn("com.google.accompanist.permissions.ExperimentalPermissionsApi") @@ -24,7 +32,6 @@ kotlin { api(libs.kotlinova.core) api(libs.kotlinova.navigation) - implementation(projects.commonNavigation) implementation(projects.sharedResources) implementation(projects.commonCompose) implementation(libs.accompanist.permissions) diff --git a/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt index 8ef765ef..00606541 100644 --- a/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt +++ b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt @@ -1,27 +1,14 @@ package com.matejdro.micropebble.home -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -59,7 +46,8 @@ class HomeScreen( Surface { HomeScreenContent( - LocalSelectedTabContent.current, + tabs = homeTabs(), + selectedContent = LocalSelectedTabContent.current, tabletMode = sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded, switchTab = { navigator.navigate(ReplaceTabContentWith(it.toScreenKey())) }, ) @@ -67,6 +55,30 @@ class HomeScreen( } } +@Composable +private fun homeTabs(): List { + val watchesIcon = painterResource(R.drawable.watches) + val watchAppsIcon = painterResource(R.drawable.watchapps) + val notificationsIcon = painterResource(R.drawable.notifications) + val toolsIcon = painterResource(R.drawable.tools) + val watchesLabel = stringResource(R.string.watches) + val watchAppsLabel = stringResource(R.string.watch_apps) + val notificationsLabel = stringResource(R.string.notifications) + val toolsLabel = stringResource(R.string.tools) + + return remember( + watchesIcon, watchAppsIcon, notificationsIcon, toolsIcon, + watchesLabel, watchAppsLabel, notificationsLabel, toolsLabel, + ) { + listOf( + HomeTab(Tab.WATCHES, watchesIcon, watchesLabel), + HomeTab(Tab.WATCH_APPS, watchAppsIcon, watchAppsLabel), + HomeTab(Tab.NOTIFICATIONS, notificationsIcon, notificationsLabel), + HomeTab(Tab.TOOLS, toolsIcon, toolsLabel), + ) + } +} + private fun Tab.toScreenKey(): ScreenKey = when (this) { Tab.WATCHES -> WatchListKey Tab.WATCH_APPS -> WatchappListKey() @@ -74,130 +86,14 @@ private fun Tab.toScreenKey(): ScreenKey = when (this) { Tab.TOOLS -> ToolsScreenKey } -@Composable -private fun HomeScreenContent( - selectedTab: SelectedTabContent, - tabletMode: Boolean, - switchTab: (Tab) -> Unit, -) { - val animatedMainContent: @Composable () -> Unit = { - AnimatedContent( - selectedTab, - contentKey = { entry -> entry.contentKey }, - transitionSpec = { fadeIn() togetherWith fadeOut() } - ) { - it.content() - } - } - if (tabletMode) { - NavigationRailContent(animatedMainContent, selectedTab.tab, switchTab) - } else { - NavigationBarContent(animatedMainContent, selectedTab.tab, switchTab) - } -} - -@Composable -private fun NavigationBarContent( - mainContent: @Composable () -> Unit, - selectedTab: Tab?, - switchTab: (Tab) -> Unit, -) { - Column { - Box( - Modifier - .fillMaxWidth() - .weight(1f) - ) { - mainContent() - } - - NavigationBar { - NavigationBarItem( - selected = selectedTab == Tab.WATCHES, - onClick = { switchTab(Tab.WATCHES) }, - icon = { Icon(painter = painterResource(id = R.drawable.watches), contentDescription = null) }, - label = { Text(stringResource(R.string.watches)) } - ) - - NavigationBarItem( - selected = selectedTab == Tab.WATCH_APPS, - onClick = { switchTab(Tab.WATCH_APPS) }, - icon = { Icon(painter = painterResource(id = R.drawable.watchapps), contentDescription = null) }, - label = { Text(stringResource(R.string.watch_apps)) } - ) - - NavigationBarItem( - selected = selectedTab == Tab.NOTIFICATIONS, - onClick = { switchTab(Tab.NOTIFICATIONS) }, - icon = { Icon(painter = painterResource(id = R.drawable.notifications), contentDescription = null) }, - label = { Text(stringResource(R.string.notifications)) } - ) - - NavigationBarItem( - selected = selectedTab == Tab.TOOLS, - onClick = { switchTab(Tab.TOOLS) }, - icon = { Icon(painter = painterResource(id = R.drawable.tools), contentDescription = null) }, - label = { Text(stringResource(R.string.tools)) } - ) - } - } -} - -@Composable -private fun NavigationRailContent( - mainContent: @Composable () -> Unit, - selectedTab: Tab?, - switchTab: (Tab) -> Unit, -) { - Row { - NavigationRail { - NavigationRailItem( - selected = selectedTab == Tab.WATCHES, - onClick = { switchTab(Tab.WATCHES) }, - icon = { Icon(painter = painterResource(id = R.drawable.watches), contentDescription = null) }, - label = { Text(stringResource(R.string.watches)) } - ) - - NavigationRailItem( - selected = selectedTab == Tab.WATCH_APPS, - onClick = { switchTab(Tab.WATCH_APPS) }, - icon = { Icon(painter = painterResource(id = R.drawable.watchapps), contentDescription = null) }, - label = { Text(stringResource(R.string.watch_apps)) } - ) - - NavigationRailItem( - selected = selectedTab == Tab.NOTIFICATIONS, - onClick = { switchTab(Tab.NOTIFICATIONS) }, - icon = { Icon(painter = painterResource(id = R.drawable.notifications), contentDescription = null) }, - label = { Text(stringResource(R.string.notifications)) } - ) - - NavigationRailItem( - selected = selectedTab == Tab.TOOLS, - onClick = { switchTab(Tab.TOOLS) }, - icon = { Icon(painter = painterResource(id = R.drawable.tools), contentDescription = null) }, - label = { Text(stringResource(R.string.tools)) } - ) - } - - Box( - Modifier - .fillMaxHeight() - .weight(1f) - ) { - mainContent() - } - } -} - @FullScreenPreviews @Composable @ShowkaseComposable(group = "Test") internal fun HomePhonePreview() { PreviewTheme { HomeScreenContent( - tabletMode = false, - selectedTab = SelectedTabContent( + tabs = homeTabs(), + selectedContent = SelectedTabContent( { Box( Modifier @@ -208,6 +104,7 @@ internal fun HomePhonePreview() { Tab.WATCH_APPS, "" ), + tabletMode = false, switchTab = {}, ) } @@ -219,8 +116,8 @@ internal fun HomePhonePreview() { internal fun HomeTabletPreview() { PreviewTheme { HomeScreenContent( - tabletMode = true, - selectedTab = SelectedTabContent( + tabs = homeTabs(), + selectedContent = SelectedTabContent( { Box( Modifier @@ -231,6 +128,7 @@ internal fun HomeTabletPreview() { Tab.WATCH_APPS, "" ), + tabletMode = true, switchTab = {}, ) } diff --git a/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeScreenContent.kt b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeScreenContent.kt new file mode 100644 index 00000000..6875eaea --- /dev/null +++ b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeScreenContent.kt @@ -0,0 +1,102 @@ +package com.matejdro.micropebble.home + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.matejdro.micropebble.navigation.keys.base.SelectedTabContent +import com.matejdro.micropebble.navigation.keys.base.Tab + +@Composable +fun HomeScreenContent( + tabs: List, + selectedContent: SelectedTabContent, + tabletMode: Boolean, + switchTab: (Tab) -> Unit, +) { + val animatedMainContent: @Composable () -> Unit = { + AnimatedContent( + selectedContent, + contentKey = { entry -> entry.contentKey }, + transitionSpec = { fadeIn() togetherWith fadeOut() } + ) { + it.content() + } + } + if (tabletMode) { + NavigationRailContent(animatedMainContent, tabs, selectedContent.tab, switchTab) + } else { + NavigationBarContent(animatedMainContent, tabs, selectedContent.tab, switchTab) + } +} + +@Composable +private fun NavigationBarContent( + mainContent: @Composable () -> Unit, + tabs: List, + selectedTab: Tab?, + switchTab: (Tab) -> Unit, +) { + Column { + Box( + Modifier + .fillMaxWidth() + .weight(1f) + ) { + mainContent() + } + + NavigationBar { + tabs.forEach { tab -> + NavigationBarItem( + selected = selectedTab == tab.tab, + onClick = { switchTab(tab.tab) }, + icon = { Icon(painter = tab.icon, contentDescription = null) }, + label = { Text(tab.label) } + ) + } + } + } +} + +@Composable +private fun NavigationRailContent( + mainContent: @Composable () -> Unit, + tabs: List, + selectedTab: Tab?, + switchTab: (Tab) -> Unit, +) { + Row { + NavigationRail { + tabs.forEach { tab -> + NavigationRailItem( + selected = selectedTab == tab.tab, + onClick = { switchTab(tab.tab) }, + icon = { Icon(painter = tab.icon, contentDescription = null) }, + label = { Text(tab.label) } + ) + } + } + + Box( + Modifier + .fillMaxHeight() + .weight(1f) + ) { + mainContent() + } + } +} diff --git a/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeTab.kt b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeTab.kt new file mode 100644 index 00000000..6052a0f3 --- /dev/null +++ b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeTab.kt @@ -0,0 +1,6 @@ +package com.matejdro.micropebble.home + +import androidx.compose.ui.graphics.painter.Painter +import com.matejdro.micropebble.navigation.keys.base.Tab + +data class HomeTab(val tab: Tab, val icon: Painter, val label: String)