diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 30a57a5d..48e1be0e 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -15,11 +15,13 @@
+
+
+
-
diff --git a/playground-shared/.gitignore b/compose-destinations-wear/.gitignore
similarity index 100%
rename from playground-shared/.gitignore
rename to compose-destinations-wear/.gitignore
diff --git a/compose-destinations-wear/build.gradle.kts b/compose-destinations-wear/build.gradle.kts
new file mode 100644
index 00000000..8d90758e
--- /dev/null
+++ b/compose-destinations-wear/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+ id("com.android.library")
+ kotlin("android")
+}
+
+apply(from = "${rootProject.projectDir}/publish.gradle")
+
+android {
+
+ compileSdk = libs.versions.compileSdk.get().toIntOrNull()
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toIntOrNull()
+ targetSdk = libs.versions.targetSdk.get().toIntOrNull()
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles.add(File("consumer-rules.pro"))
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
+ }
+}
+
+tasks.withType().configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
+}
+
+dependencies {
+ api(project(mapOf("path" to ":compose-destinations")))
+
+ api(libs.wear.compose.navigation)
+}
diff --git a/compose-destinations-wear/consumer-rules.pro b/compose-destinations-wear/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/compose-destinations-wear/gradle.properties b/compose-destinations-wear/gradle.properties
new file mode 100644
index 00000000..b96767b2
--- /dev/null
+++ b/compose-destinations-wear/gradle.properties
@@ -0,0 +1,2 @@
+POM_ARTIFACT_ID=wear-core
+POM_NAME=wear-core
\ No newline at end of file
diff --git a/compose-destinations-wear/proguard-rules.pro b/compose-destinations-wear/proguard-rules.pro
new file mode 100644
index 00000000..ff59496d
--- /dev/null
+++ b/compose-destinations-wear/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/compose-destinations-wear/src/main/AndroidManifest.xml b/compose-destinations-wear/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..1b3a8438
--- /dev/null
+++ b/compose-destinations-wear/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/compose-destinations-wear/src/main/java/com/ramcosta/composedestinations/wear/WearNavHostEngine.kt b/compose-destinations-wear/src/main/java/com/ramcosta/composedestinations/wear/WearNavHostEngine.kt
new file mode 100644
index 00000000..84966b9d
--- /dev/null
+++ b/compose-destinations-wear/src/main/java/com/ramcosta/composedestinations/wear/WearNavHostEngine.kt
@@ -0,0 +1,160 @@
+package com.ramcosta.composedestinations.wear
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.navigation.*
+import androidx.wear.compose.navigation.*
+import com.ramcosta.composedestinations.annotation.InternalDestinationsApi
+import com.ramcosta.composedestinations.manualcomposablecalls.DestinationLambda
+import com.ramcosta.composedestinations.manualcomposablecalls.ManualComposableCalls
+import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder
+import com.ramcosta.composedestinations.rememberNavHostEngine
+import com.ramcosta.composedestinations.scope.DestinationScopeImpl
+import com.ramcosta.composedestinations.spec.*
+
+/**
+ * Returns the [WearNavHostEngine] to be used with Wear OS apps.
+ */
+@Composable
+fun rememberWearNavHostEngine(
+ state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(),
+): NavHostEngine {
+ val defaultNavHostEngine = rememberNavHostEngine()
+
+ return remember {
+ WearNavHostEngine(defaultNavHostEngine, state)
+ }
+}
+
+internal class WearNavHostEngine(
+ private val defaultNavHostEngine: NavHostEngine,
+ private val state: SwipeDismissableNavHostState,
+) : NavHostEngine {
+
+ override val type = NavHostEngine.Type.WEAR
+
+ @Composable
+ override fun rememberNavController(
+ vararg navigators: Navigator
+ ) =
+ androidx.navigation.compose.rememberNavController(remember { WearNavigator() }, *navigators)
+
+ @Composable
+ override fun NavHost(
+ modifier: Modifier,
+ route: String,
+ startRoute: Route,
+ navController: NavHostController,
+ builder: NavGraphBuilder.() -> Unit
+ ) {
+ SwipeDismissableNavHost(
+ navController = navController,
+ startDestination = startRoute.route,
+ modifier = modifier,
+ route = route,
+ state = state,
+ builder = builder
+ )
+ }
+
+ override fun NavGraphBuilder.navigation(
+ navGraph: NavGraphSpec,
+ builder: NavGraphBuilder.() -> Unit
+ ) {
+ with(defaultNavHostEngine) { navigation(navGraph, builder) }
+ }
+
+ @OptIn(ExperimentalAnimationApi::class, InternalDestinationsApi::class)
+ override fun NavGraphBuilder.composable(
+ destination: DestinationSpec,
+ navController: NavHostController,
+ dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
+ manualComposableCalls: ManualComposableCalls,
+ ) {
+ when (destination.style) {
+ is DestinationStyle.Runtime,
+ is DestinationStyle.Default -> {
+ addComposable(
+ destination,
+ navController,
+ dependenciesContainerBuilder,
+ manualComposableCalls
+ )
+ }
+
+ is DestinationStyle.Activity -> {
+ with(defaultNavHostEngine) {
+ composable(destination, navController, dependenciesContainerBuilder, manualComposableCalls)
+ }
+ }
+
+ is DestinationStyle.Dialog,
+ is DestinationStyle.Animated,
+ is DestinationStyle.BottomSheet -> {
+ throw IllegalStateException("${destination.style.javaClass.name} cannot be used on Wear OS version of the core library!")
+ }
+ }
+ }
+
+ private fun NavGraphBuilder.addComposable(
+ destination: DestinationSpec,
+ navController: NavHostController,
+ dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
+ manualComposableCalls: ManualComposableCalls,
+ ) {
+ @SuppressLint("RestrictedApi")
+ val contentLambda = manualComposableCalls[destination.baseRoute]
+
+ composable(
+ route = destination.route,
+ arguments = destination.arguments,
+ deepLinks = destination.deepLinks
+ ) { navBackStackEntry ->
+ CallComposable(
+ destination,
+ navController,
+ navBackStackEntry,
+ dependenciesContainerBuilder,
+ contentLambda
+ )
+ }
+ }
+
+ internal class WearDestinationScope(
+ destination: DestinationSpec,
+ navBackStackEntry: NavBackStackEntry,
+ navController: NavController,
+ ) : DestinationScopeImpl(
+ destination,
+ navBackStackEntry,
+ navController,
+ )
+
+ @Suppress("UNCHECKED_CAST")
+ @Composable
+ private fun CallComposable(
+ destination: DestinationSpec,
+ navController: NavHostController,
+ navBackStackEntry: NavBackStackEntry,
+ dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
+ contentLambda: DestinationLambda<*>?
+ ) {
+ val scope = remember {
+ WearDestinationScope(
+ destination,
+ navBackStackEntry,
+ navController
+ )
+ }
+
+ if (contentLambda == null) {
+ with(destination) { scope.Content(dependenciesContainerBuilder) }
+ } else {
+ contentLambda as DestinationLambda
+ contentLambda(scope)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt b/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt
index 40f0bce2..5856b449 100644
--- a/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt
+++ b/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt
@@ -113,7 +113,7 @@ class ManualComposableCallsBuilder internal constructor(
private fun ManualComposableCallsBuilder.validateAnimated(
destination: DestinationSpec<*>
) {
- if (engineType == NavHostEngine.Type.DEFAULT) {
+ if (engineType != NavHostEngine.Type.ANIMATED) {
error("'animatedComposable' can only be called with a 'AnimatedNavHostEngine'")
}
@@ -125,7 +125,7 @@ private fun ManualComposableCallsBuilder.validateAnimated(
private fun ManualComposableCallsBuilder.validateBottomSheet(
destination: DestinationSpec<*>
) {
- if (engineType == NavHostEngine.Type.DEFAULT) {
+ if (engineType != NavHostEngine.Type.ANIMATED) {
error("'bottomSheetComposable' can only be called with a 'AnimatedNavHostEngine'")
}
diff --git a/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/NavHostEngine.kt b/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/NavHostEngine.kt
index 79e6baff..53f18f41 100644
--- a/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/NavHostEngine.kt
+++ b/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/NavHostEngine.kt
@@ -29,7 +29,13 @@ interface NavHostEngine {
* The engine you get if using "io.github.raamcosta.compose-destinations:animations-core"
* and calling `rememberAnimatedNavHostEngine`
*/
- ANIMATED
+ ANIMATED,
+
+ /**
+ * The engine you get if using "io.github.raamcosta.compose-destinations:wear-core"
+ * and calling `rememberWearNavHostEngine`
+ */
+ WEAR
}
/**
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7ff8f407..832a8a26 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -29,6 +29,7 @@ ktxSerialization = "1.4.1"
mockk = "1.13.2"
compileTesting = "1.4.9"
+composeWear = "1.1.0"
[plugins]
dependencyCheckPlugin = { id = "com.github.ben-manes.versions", version.ref = "dependencyCheckPluginVersion" }
@@ -69,3 +70,10 @@ test-junit = { module = "junit:junit", version.ref = "junit" }
test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
test-kotlinCompile = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "compileTesting" }
test-kotlinCompileKsp = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "compileTesting" }
+
+# Wear
+
+wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "composeWear" }
+wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "composeWear" }
+wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "composeWear" }
+wear-input = { module = "androidx.wear:wear-input", version.ref = "composeWear" }
diff --git a/playground-core/.gitignore b/playground-core/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/playground-core/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/playground-shared/build.gradle.kts b/playground-core/build.gradle.kts
similarity index 72%
rename from playground-shared/build.gradle.kts
rename to playground-core/build.gradle.kts
index cc30663e..e8259cee 100644
--- a/playground-shared/build.gradle.kts
+++ b/playground-core/build.gradle.kts
@@ -1,10 +1,9 @@
plugins {
id("java-library")
- id("kotlin")
- kotlin("plugin.serialization")
+ id("org.jetbrains.kotlin.jvm")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
-}
+}
\ No newline at end of file
diff --git a/playground-shared/src/main/java/com/ramcosta/samples/playgroundshared/BlogPostArgs.kt b/playground-core/src/main/java/com/ramcosta/playground/core/BlogPostArgs.kt
similarity index 51%
rename from playground-shared/src/main/java/com/ramcosta/samples/playgroundshared/BlogPostArgs.kt
rename to playground-core/src/main/java/com/ramcosta/playground/core/BlogPostArgs.kt
index daf32966..fa73b258 100644
--- a/playground-shared/src/main/java/com/ramcosta/samples/playgroundshared/BlogPostArgs.kt
+++ b/playground-core/src/main/java/com/ramcosta/playground/core/BlogPostArgs.kt
@@ -1,4 +1,4 @@
-package com.ramcosta.samples.playgroundshared
+package com.ramcosta.playground.core
data class BlogPostArgs(
val slug: String
diff --git a/playground-shared/src/main/java/com/ramcosta/samples/playgroundshared/WithDefaultValueArgs.kt b/playground-core/src/main/java/com/ramcosta/playground/core/WithDefaultValueArgs.kt
similarity index 60%
rename from playground-shared/src/main/java/com/ramcosta/samples/playgroundshared/WithDefaultValueArgs.kt
rename to playground-core/src/main/java/com/ramcosta/playground/core/WithDefaultValueArgs.kt
index c8d6a8b5..08ae29f7 100644
--- a/playground-shared/src/main/java/com/ramcosta/samples/playgroundshared/WithDefaultValueArgs.kt
+++ b/playground-core/src/main/java/com/ramcosta/playground/core/WithDefaultValueArgs.kt
@@ -1,4 +1,4 @@
-package com.ramcosta.samples.playgroundshared
+package com.ramcosta.playground.core
data class WithDefaultValueArgs(
val isCreate: Boolean = false,
diff --git a/playground/build.gradle.kts b/playground/build.gradle.kts
index a782dbc2..a78ff589 100644
--- a/playground/build.gradle.kts
+++ b/playground/build.gradle.kts
@@ -96,7 +96,7 @@ android {
dependencies {
implementation(project(mapOf("path" to ":compose-destinations-animations")))
- implementation(project(mapOf("path" to ":playground-shared")))
+ implementation(project(mapOf("path" to ":playground-core")))
ksp(project(":compose-destinations-ksp"))
implementation(libs.androidMaterial)
diff --git a/playground/src/test/kotlin/com/ramcosta/composedestinations/ksp/ProcessorProviderTests.kt b/playground/src/test/kotlin/com/ramcosta/composedestinations/ksp/ProcessorProviderTests.kt
index 8c50efb2..79d22587 100644
--- a/playground/src/test/kotlin/com/ramcosta/composedestinations/ksp/ProcessorProviderTests.kt
+++ b/playground/src/test/kotlin/com/ramcosta/composedestinations/ksp/ProcessorProviderTests.kt
@@ -363,7 +363,7 @@ class ProcessorProviderTests {
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
- import com.ramcosta.samples.playgroundshared.BlogPostArgs
+ import com.ramcosta.playground.core.BlogPostArgs
@RootNavGraph(start = true)
@Destination(route = "test1")
@@ -398,7 +398,7 @@ class ProcessorProviderTests {
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
- import com.ramcosta.samples.playgroundshared.WithDefaultValueArgs
+ import com.ramcosta.playground.core.WithDefaultValueArgs
@RootNavGraph(start = true)
@Destination(route = "test1")
diff --git a/sample-wear/.gitignore b/sample-wear/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/sample-wear/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/sample-wear/build.gradle.kts b/sample-wear/build.gradle.kts
new file mode 100644
index 00000000..01afaf94
--- /dev/null
+++ b/sample-wear/build.gradle.kts
@@ -0,0 +1,80 @@
+plugins {
+ id("com.android.application")
+ kotlin("android")
+ id("com.google.devtools.ksp") version libs.versions.ksp.get()
+}
+
+tasks.withType().configureEach {
+ kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
+}
+
+android {
+ compileSdk = 33
+
+ defaultConfig {
+ applicationId = "com.ramcosta.destinations.sample.wear"
+ minSdk = 25
+ targetSdk = 33
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
+ }
+
+ packagingOptions {
+ resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}")
+ }
+
+ applicationVariants.all {
+ kotlin.sourceSets {
+ getByName(name) {
+ kotlin.srcDir("build/generated/ksp/$name/kotlin")
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation(project(mapOf("path" to ":compose-destinations-wear")))
+ ksp(project(":compose-destinations-ksp"))
+
+ implementation(libs.androidMaterial)
+
+ implementation(libs.compose.ui)
+ implementation(libs.compose.material)
+ implementation(libs.compose.viewModel)
+
+ implementation(libs.androidx.lifecycleRuntimeKtx)
+ implementation(libs.androidx.activityCompose)
+
+ implementation(libs.wear.compose.foundation)
+ implementation(libs.wear.compose.material)
+ implementation(libs.wear.input)
+}
diff --git a/sample-wear/proguard-rules.pro b/sample-wear/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/sample-wear/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/sample-wear/src/main/AndroidManifest.xml b/sample-wear/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..f8e66e5e
--- /dev/null
+++ b/sample-wear/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/MainActivity.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/MainActivity.kt
new file mode 100644
index 00000000..c46bca75
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/MainActivity.kt
@@ -0,0 +1,30 @@
+package com.ramcosta.destinations.sample.wear
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.staticCompositionLocalOf
+import com.ramcosta.destinations.sample.wear.core.di.DependencyContainer
+import com.ramcosta.destinations.sample.wear.ui.theme.DestinationsTodoSampleTheme
+
+val LocalDependencyContainer = staticCompositionLocalOf {
+ error("No dependency container provided!")
+}
+
+class MainActivity : ComponentActivity() {
+
+ private val dependencyContainer by lazy { DependencyContainer(this) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ DestinationsTodoSampleTheme {
+ CompositionLocalProvider(LocalDependencyContainer provides dependencyContainer) {
+ SampleApp()
+ }
+ }
+ }
+ }
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/MainViewModel.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/MainViewModel.kt
new file mode 100644
index 00000000..a7e010e4
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/MainViewModel.kt
@@ -0,0 +1,21 @@
+package com.ramcosta.destinations.sample.wear
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.destinations.sample.wear.login.data.LoginStateRepository
+import kotlinx.coroutines.launch
+
+class MainViewModel(
+ private val loginStateRepository: LoginStateRepository
+) : ViewModel() {
+
+ val isLoggedInFlow = loginStateRepository.isLoggedIn
+
+ val isLoggedIn get() = isLoggedInFlow.value
+
+ fun login() {
+ viewModelScope.launch {
+ loginStateRepository.login()
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/SampleApp.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/SampleApp.kt
new file mode 100644
index 00000000..ebcbfb65
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/SampleApp.kt
@@ -0,0 +1,61 @@
+package com.ramcosta.destinations.sample.wear
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.navigation.NavHostController
+import androidx.wear.compose.material.rememberSwipeToDismissBoxState
+import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState
+import com.ramcosta.composedestinations.DestinationsNavHost
+import com.ramcosta.composedestinations.navigation.navigate
+import com.ramcosta.composedestinations.wear.rememberWearNavHostEngine
+import com.ramcosta.destinations.sample.wear.core.viewmodel.activityViewModel
+import com.ramcosta.destinations.sample.wear.destinations.Destination
+import com.ramcosta.destinations.sample.wear.destinations.LoginScreenDestination
+import com.ramcosta.destinations.sample.wear.ui.composables.SampleScaffold
+
+@Composable
+fun SampleApp() {
+ val engine = rememberWearNavHostEngine()
+ val navController = engine.rememberNavController()
+
+ val vm = activityViewModel()
+ // 👇 this avoids a jump in the UI that would happen if we relied only on ShowLoginWhenLoggedOut
+ val startRoute = if (!vm.isLoggedIn) LoginScreenDestination else NavGraphs.root.startRoute
+
+ SampleScaffold(
+ navController = navController,
+ startRoute = startRoute,
+ ) {
+ DestinationsNavHost(
+ engine = engine,
+ navController = navController,
+ navGraph = NavGraphs.root,
+ startRoute = startRoute
+ )
+
+ // Has to be called after calling DestinationsNavHost because only
+ // then does NavController have a graph associated that we need for
+ // `appCurrentDestinationAsState` method
+ ShowLoginWhenLoggedOut(vm, navController)
+ }
+}
+
+val Destination.shouldShowScaffoldElements get() = this !is LoginScreenDestination
+
+@Composable
+private fun ShowLoginWhenLoggedOut(
+ vm: MainViewModel,
+ navController: NavHostController
+) {
+ val currentDestination by navController.appCurrentDestinationAsState()
+ val isLoggedIn by vm.isLoggedInFlow.collectAsState()
+
+ if (!isLoggedIn && currentDestination != LoginScreenDestination) {
+ // everytime destination changes or logged in state we check
+ // if we have to show Login screen and navigate to it if so
+ navController.navigate(LoginScreenDestination) {
+ launchSingleTop = true
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/account/AccountScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/account/AccountScreen.kt
new file mode 100644
index 00000000..842d1719
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/account/AccountScreen.kt
@@ -0,0 +1,31 @@
+package com.ramcosta.destinations.sample.wear.account
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.Text
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.destinations.sample.wear.core.viewmodel.viewModel
+
+@Destination
+@Composable
+fun AccountScreen(
+ vm: AccountViewModel = viewModel(),
+) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Button(onClick = { vm.onLogoutClick() }) {
+ Text("Logout", modifier = Modifier.padding(6.dp))
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/account/AccountViewModel.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/account/AccountViewModel.kt
new file mode 100644
index 00000000..1e5ce08f
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/account/AccountViewModel.kt
@@ -0,0 +1,17 @@
+package com.ramcosta.destinations.sample.wear.account
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.destinations.sample.wear.login.data.LoginStateRepository
+import kotlinx.coroutines.launch
+
+class AccountViewModel(
+ private val loginStateRepository: LoginStateRepository
+) : ViewModel() {
+
+ fun onLogoutClick() {
+ viewModelScope.launch {
+ loginStateRepository.logout()
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/core/di/DependencyContainer.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/core/di/DependencyContainer.kt
new file mode 100644
index 00000000..8258651e
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/core/di/DependencyContainer.kt
@@ -0,0 +1,53 @@
+package com.ramcosta.destinations.sample.wear.core.di
+
+import androidx.lifecycle.SavedStateHandle
+import com.ramcosta.destinations.sample.wear.MainActivity
+import com.ramcosta.destinations.sample.wear.MainViewModel
+import com.ramcosta.destinations.sample.wear.account.AccountViewModel
+import com.ramcosta.destinations.sample.wear.login.data.LoginStateRepository
+import com.ramcosta.destinations.sample.wear.navArgs
+import com.ramcosta.destinations.sample.wear.tasks.data.StepsRepository
+import com.ramcosta.destinations.sample.wear.tasks.data.TasksRepository
+import com.ramcosta.destinations.sample.wear.tasks.presentation.details.StepDetailsViewModel
+import com.ramcosta.destinations.sample.wear.tasks.presentation.details.StepScreenNavArgs
+import com.ramcosta.destinations.sample.wear.tasks.presentation.details.TaskDetailsViewModel
+import com.ramcosta.destinations.sample.wear.tasks.presentation.list.TaskListViewModel
+import com.ramcosta.destinations.sample.wear.tasks.presentation.new.AddStepDialogNavArgs
+import com.ramcosta.destinations.sample.wear.tasks.presentation.new.AddStepViewModel
+import com.ramcosta.destinations.sample.wear.tasks.presentation.new.AddTaskViewModel
+
+class DependencyContainer(
+ val activity: MainActivity
+) {
+
+ val loginStateRepository: LoginStateRepository by lazy { LoginStateRepository() }
+
+ val tasksRepository: TasksRepository by lazy { TasksRepository(stepsRepository) }
+
+ val stepsRepository: StepsRepository by lazy { StepsRepository() }
+
+ @Suppress("UNCHECKED_CAST")
+ fun createViewModel(modelClass: Class, handle: SavedStateHandle): T {
+ return when (modelClass) {
+ MainViewModel::class.java -> MainViewModel(loginStateRepository)
+ AccountViewModel::class.java -> AccountViewModel(loginStateRepository)
+ TaskListViewModel::class.java -> TaskListViewModel(tasksRepository, stepsRepository)
+ AddTaskViewModel::class.java -> AddTaskViewModel(tasksRepository)
+ AddStepViewModel::class.java -> AddStepViewModel(
+ handle.navArgs().taskId,
+ stepsRepository
+ )
+ TaskDetailsViewModel::class.java -> TaskDetailsViewModel(
+ handle,
+ tasksRepository,
+ stepsRepository
+ )
+ StepDetailsViewModel::class.java -> StepDetailsViewModel(
+ handle.navArgs().stepId,
+ tasksRepository,
+ stepsRepository
+ )
+ else -> throw RuntimeException("Unknown view model $modelClass")
+ } as T
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/core/viewmodel/ViewModelUtils.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/core/viewmodel/ViewModelUtils.kt
new file mode 100644
index 00000000..62528ecd
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/core/viewmodel/ViewModelUtils.kt
@@ -0,0 +1,65 @@
+package com.ramcosta.destinations.sample.wear.core.viewmodel
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.navigation.NavBackStackEntry
+import androidx.savedstate.SavedStateRegistryOwner
+import com.ramcosta.destinations.sample.wear.LocalDependencyContainer
+import com.ramcosta.destinations.sample.wear.core.di.DependencyContainer
+
+@Composable
+inline fun viewModel(
+ viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
+ "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
+ },
+ savedStateRegistryOwner: SavedStateRegistryOwner = LocalSavedStateRegistryOwner.current
+): VM {
+ return androidx.lifecycle.viewmodel.compose.viewModel(
+ viewModelStoreOwner = viewModelStoreOwner,
+ factory = ViewModelFactory(
+ owner = savedStateRegistryOwner,
+ defaultArgs = (savedStateRegistryOwner as? NavBackStackEntry)?.arguments,
+ dependencyContainer = LocalDependencyContainer.current,
+ )
+ )
+}
+
+@Composable
+inline fun activityViewModel(): VM {
+ val activity = LocalDependencyContainer.current.activity
+
+ return androidx.lifecycle.viewmodel.compose.viewModel(
+ VM::class.java,
+ activity,
+ null,
+ factory = ViewModelFactory(
+ owner = activity,
+ defaultArgs = null,
+ dependencyContainer = LocalDependencyContainer.current,
+ )
+ )
+}
+
+class ViewModelFactory(
+ owner: SavedStateRegistryOwner,
+ defaultArgs: Bundle?,
+ private val dependencyContainer: DependencyContainer
+) : AbstractSavedStateViewModelFactory(
+ owner,
+ defaultArgs
+) {
+
+ override fun create(
+ key: String,
+ modelClass: Class,
+ handle: SavedStateHandle
+ ): T {
+ return dependencyContainer.createViewModel(modelClass, handle)
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/login/LoginScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/login/LoginScreen.kt
new file mode 100644
index 00000000..6ff6d8f5
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/login/LoginScreen.kt
@@ -0,0 +1,68 @@
+package com.ramcosta.destinations.sample.wear.login
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.Text
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.navigation.popUpTo
+import com.ramcosta.destinations.sample.wear.MainViewModel
+import com.ramcosta.destinations.sample.wear.NavGraphs
+import com.ramcosta.destinations.sample.wear.core.viewmodel.activityViewModel
+import com.ramcosta.destinations.sample.wear.destinations.DirectionDestination
+import com.ramcosta.destinations.sample.wear.destinations.LoginScreenDestination
+import com.ramcosta.destinations.sample.wear.startAppDestination
+
+@Destination
+@Composable
+fun LoginScreen(
+ vm: MainViewModel = activityViewModel(),
+ navigator: DestinationsNavigator
+) {
+ BackHandler(true) { /* We want to disable back clicks */ }
+
+ val isLoggedIn by vm.isLoggedInFlow.collectAsState()
+ val hasNavigatedUp = remember { mutableStateOf(false) }
+
+ if (isLoggedIn && !hasNavigatedUp.value) {
+ hasNavigatedUp.value = true // avoids double navigation
+
+ if (!navigator.navigateUp()) {
+ // Sometimes we are starting on LoginScreen (to avoid UI jumps)
+ // In those cases, navigateUp fails, so we just navigate to the registered start destination
+ navigator.navigate(NavGraphs.root.startAppDestination as DirectionDestination) {
+ popUpTo(LoginScreenDestination) {
+ inclusive = true
+ }
+ }
+ }
+ }
+
+ LoginScreenContent { vm.login() }
+}
+
+@Composable
+private fun LoginScreenContent(onLoginClick: () -> Unit) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Text("WORK IN PROGRESS")
+
+ Spacer(Modifier.height(32.dp))
+
+ Chip(onClick = onLoginClick, label = {
+ Text("Login")
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/login/data/LoginStateRepository.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/login/data/LoginStateRepository.kt
new file mode 100644
index 00000000..e55ba136
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/login/data/LoginStateRepository.kt
@@ -0,0 +1,21 @@
+package com.ramcosta.destinations.sample.wear.login.data
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.withContext
+
+class LoginStateRepository {
+
+ private val _isLoggedIn = MutableStateFlow(false)
+ val isLoggedIn = _isLoggedIn.asStateFlow()
+
+ suspend fun login() = withContext(Dispatchers.IO) {
+ _isLoggedIn.update { true }
+ }
+
+ suspend fun logout() = withContext(Dispatchers.IO) {
+ _isLoggedIn.update { false }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/settings/SettingScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/settings/SettingScreen.kt
new file mode 100644
index 00000000..0496ae69
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/settings/SettingScreen.kt
@@ -0,0 +1,20 @@
+package com.ramcosta.destinations.sample.wear.settings
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material.Text
+import com.ramcosta.composedestinations.annotation.Destination
+
+@Destination
+@Composable
+fun SettingsScreen() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("Settings")
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/data/StepsRepository.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/data/StepsRepository.kt
new file mode 100644
index 00000000..89e2393e
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/data/StepsRepository.kt
@@ -0,0 +1,55 @@
+package com.ramcosta.destinations.sample.wear.tasks.data
+
+import com.ramcosta.destinations.sample.wear.tasks.domain.Step
+import com.ramcosta.destinations.sample.wear.tasks.domain.Task
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.withContext
+import java.util.concurrent.atomic.AtomicInteger
+
+class StepsRepository {
+
+ private val nextId = AtomicInteger(0)
+
+ private val _steps = MutableStateFlow>(emptyList())
+ val steps = _steps.asStateFlow()
+
+ suspend fun removeStep(step: Step) = withContext(Dispatchers.IO) {
+ _steps.update { it - step }
+ }
+
+ fun stepsByTask(taskId: Int): Flow> {
+ return _steps.map {
+ it.filter { it.taskId == taskId }
+ }
+ }
+
+ suspend fun removeStepsForTask(task: Task) = withContext(Dispatchers.IO) {
+ _steps.update {
+ it.toMutableList().apply { removeAll { it.taskId == task.id } }
+ }
+ }
+
+ suspend fun updateStep(step: Step) = withContext(Dispatchers.IO) {
+ _steps.update {
+ val idx = it.indexOfFirst { it.id == step.id }
+ if (idx != -1) {
+ it.toMutableList().apply {
+ removeAt(idx)
+ add(idx, step)
+ }
+ } else {
+ it
+ }
+ }
+ }
+
+ suspend fun addNewStep(step: Step) = withContext(Dispatchers.IO) {
+ val stepWithRightId = step.copy(id = nextId.getAndAdd(1))
+ _steps.update { it + stepWithRightId }
+ }
+
+ fun stepForId(stepId: Int): Flow {
+ return _steps.map { it.first { it.id == stepId } }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/data/TasksRepository.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/data/TasksRepository.kt
new file mode 100644
index 00000000..be59128f
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/data/TasksRepository.kt
@@ -0,0 +1,53 @@
+package com.ramcosta.destinations.sample.wear.tasks.data
+
+import com.ramcosta.destinations.sample.wear.tasks.domain.Task
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.withContext
+
+class TasksRepository(
+ private val stepsRepository: StepsRepository
+) {
+
+ private val _tasks: MutableStateFlow> = MutableStateFlow>(mutableListOf()).apply {
+ value = mutableListOf(
+ Task("Task #1", "Description #1", false, 1),
+ Task("Task #2", "Description #2", false, 2),
+ Task("Task #3", "Description #3", false, 3),
+ Task("Task #4", "Description #4", false, 4),
+ Task("Task #5", "Description #5", false, 5),
+ Task("Task #6", "Description #6", false, 6),
+ )
+ }
+ val tasks: StateFlow> = _tasks.asStateFlow()
+
+ fun taskById(id: Int): Flow = _tasks.map { it.firstOrNull { it.id == id } }
+
+ suspend fun addNewTask(task: Task) = withContext(Dispatchers.IO) {
+ _tasks.update {
+ it.toMutableList().apply {
+ add(task.copy(id = (it.lastOrNull()?.id ?: -1) + 1))
+ }
+ }
+ }
+
+ suspend fun updateTask(task: Task) = withContext(Dispatchers.IO) {
+ _tasks.update {
+ it.toMutableList().apply {
+ val idx = indexOfFirst { it.id == task.id }
+ if (idx != -1) {
+ removeAt(idx)
+ add(idx, task)
+ }
+ }
+ }
+ }
+
+ suspend fun deleteTask(task: Task) = withContext(Dispatchers.IO) {
+ _tasks.update {
+ it.toMutableList().apply { remove(task) }
+ }
+
+ stepsRepository.removeStepsForTask(task)
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/domain/Step.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/domain/Step.kt
new file mode 100644
index 00000000..10af2605
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/domain/Step.kt
@@ -0,0 +1,8 @@
+package com.ramcosta.destinations.sample.wear.tasks.domain
+
+data class Step(
+ val taskId: Int,
+ val title: String,
+ val completed: Boolean,
+ val id: Int = 0,
+)
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/domain/Task.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/domain/Task.kt
new file mode 100644
index 00000000..5d8657e2
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/domain/Task.kt
@@ -0,0 +1,8 @@
+package com.ramcosta.destinations.sample.wear.tasks.domain
+
+data class Task(
+ val title: String,
+ val description: String,
+ val completed: Boolean,
+ val id: Int = 0,
+)
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepDetailsViewModel.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepDetailsViewModel.kt
new file mode 100644
index 00000000..d25ea8aa
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepDetailsViewModel.kt
@@ -0,0 +1,35 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.details
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.destinations.sample.wear.tasks.data.StepsRepository
+import com.ramcosta.destinations.sample.wear.tasks.data.TasksRepository
+import com.ramcosta.destinations.sample.wear.tasks.domain.Step
+import com.ramcosta.destinations.sample.wear.tasks.domain.Task
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+
+class StepDetailsViewModel(
+ private val stepId: Int,
+ private val tasksRepository: TasksRepository,
+ private val stepsRepository: StepsRepository
+): ViewModel() {
+
+ val step: StateFlow = stepsRepository.stepForId(stepId)
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
+
+ @OptIn(FlowPreview::class)
+ val task: StateFlow = stepsRepository.stepForId(stepId)
+ .flatMapConcat {
+ tasksRepository.taskById(it.taskId)
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
+
+ fun onStepCheckedChange(checked: Boolean) {
+ val value = step.value ?: return
+ viewModelScope.launch {
+ stepsRepository.updateStep(value.copy(completed = checked))
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepScreen.kt
new file mode 100644
index 00000000..00ec3eb6
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepScreen.kt
@@ -0,0 +1,49 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.details
+
+import androidx.compose.foundation.layout.*
+import androidx.wear.compose.material.Checkbox
+import androidx.wear.compose.material.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.Text
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.destinations.sample.wear.core.viewmodel.viewModel
+
+@Destination(navArgsDelegate = StepScreenNavArgs::class)
+@Composable
+fun StepScreen(
+ viewModel: StepDetailsViewModel = viewModel()
+) {
+ val step = viewModel.step.collectAsState().value
+
+ if (step == null) {
+ CircularProgressIndicator()
+ return
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize()
+ .padding(8.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("Completed")
+
+ Checkbox(
+ checked = step.completed,
+ onCheckedChange = viewModel::onStepCheckedChange
+ )
+ }
+
+ Spacer(Modifier.height(16.dp))
+ }
+ }
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepScreenNavArgs.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepScreenNavArgs.kt
new file mode 100644
index 00000000..2258148e
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/StepScreenNavArgs.kt
@@ -0,0 +1,5 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.details
+
+data class StepScreenNavArgs(
+ val stepId: Int
+)
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskDetailsViewModel.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskDetailsViewModel.kt
new file mode 100644
index 00000000..d06f94e9
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskDetailsViewModel.kt
@@ -0,0 +1,42 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.details
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.destinations.sample.wear.navArgs
+import com.ramcosta.destinations.sample.wear.tasks.data.StepsRepository
+import com.ramcosta.destinations.sample.wear.tasks.data.TasksRepository
+import com.ramcosta.destinations.sample.wear.tasks.domain.Step
+import com.ramcosta.destinations.sample.wear.tasks.domain.Task
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class TaskDetailsViewModel(
+ savedStateHandle: SavedStateHandle,
+ private val tasksRepository: TasksRepository,
+ private val stepsRepository: StepsRepository
+) : ViewModel() {
+
+ private val navArgs: TaskScreenNavArgs = savedStateHandle.navArgs()
+
+ val task: StateFlow = tasksRepository.taskById(navArgs.taskId)
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
+
+ val steps: StateFlow> = stepsRepository.stepsByTask(navArgs.taskId)
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ fun onStepCheckedChanged(step: Step, checked: Boolean) {
+ viewModelScope.launch {
+ stepsRepository.updateStep(step.copy(completed = checked))
+ }
+ }
+
+ fun onTaskCheckedChange(checked: Boolean) {
+ val task = task.value ?: return
+ viewModelScope.launch {
+ tasksRepository.updateTask(task.copy(completed = checked))
+ }
+ }
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskScreen.kt
new file mode 100644
index 00000000..a87c07ef
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskScreen.kt
@@ -0,0 +1,105 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.details
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.wear.compose.material.*
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.destinations.sample.wear.core.viewmodel.viewModel
+import com.ramcosta.destinations.sample.wear.destinations.AddStepScreenDestination
+import com.ramcosta.destinations.sample.wear.destinations.StepScreenDestination
+import com.ramcosta.destinations.sample.wear.tasks.domain.Step
+
+@Destination(navArgsDelegate = TaskScreenNavArgs::class)
+@Composable
+fun TaskScreen(
+ navArgs: TaskScreenNavArgs,
+ navigator: DestinationsNavigator,
+ viewModel: TaskDetailsViewModel = viewModel()
+) {
+ val task = viewModel.task.collectAsState().value
+
+ if (task == null) {
+ CircularProgressIndicator()
+ return
+ }
+
+ val steps = viewModel.steps.collectAsState().value
+
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ Button(
+ onClick = { navigator.navigate(AddStepScreenDestination(navArgs.taskId)) }) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = "add step button"
+ )
+ }
+ }
+
+ item {
+ ToggleChip(
+ modifier = Modifier.fillMaxWidth(),
+ checked = task.completed,
+ onCheckedChange = viewModel::onTaskCheckedChange,
+ label = { Text("Completed:") },
+ toggleControl = {
+ Icon(
+ imageVector = ToggleChipDefaults.checkboxIcon(checked = task.completed),
+ contentDescription = if (task.completed) "On" else "Off"
+ )
+ }
+ )
+ }
+
+ item {
+ ListHeader {
+ Text("Steps")
+ }
+ }
+
+ if (steps.isEmpty()) {
+ item {
+ Text("None", style = MaterialTheme.typography.body2)
+ }
+ }
+
+ items(steps) { step ->
+ StepItem(
+ step = step,
+ onStepClicked = {
+ navigator.navigate(StepScreenDestination(step.id))
+ },
+ onCheckedChange = { viewModel.onStepCheckedChanged(step, it) }
+ )
+ }
+ }
+}
+
+@Composable
+fun StepItem(
+ step: Step,
+ onStepClicked: () -> Unit,
+ onCheckedChange: (Boolean) -> Unit
+) {
+ SplitToggleChip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = onStepClicked,
+ checked = step.completed,
+ onCheckedChange = onCheckedChange,
+ label = { Text(step.title) },
+ toggleControl = {
+ Icon(
+ imageVector = ToggleChipDefaults.checkboxIcon(checked = step.completed),
+ contentDescription = if (step.completed) "On" else "Off"
+ )
+ }
+ )
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskScreenNavArgs.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskScreenNavArgs.kt
new file mode 100644
index 00000000..82a7ff43
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/details/TaskScreenNavArgs.kt
@@ -0,0 +1,5 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.details
+
+data class TaskScreenNavArgs(
+ val taskId: Int
+)
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskItem.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskItem.kt
new file mode 100644
index 00000000..f91aaf2c
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskItem.kt
@@ -0,0 +1,37 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.list
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.SplitToggleChip
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.ToggleChipDefaults
+
+@Composable
+fun TaskItem(
+ task: TaskUiItem,
+ onCheckedChange: (Boolean) -> Unit,
+ onTaskClicked: () -> Unit
+) {
+ SplitToggleChip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = onTaskClicked,
+ checked = task.task.completed,
+ onCheckedChange = onCheckedChange,
+ label = {
+ Text(task.task.title)
+ },
+ toggleControl = {
+ Icon(
+ imageVector = ToggleChipDefaults.checkboxIcon(checked = task.task.completed),
+ contentDescription = if (task.task.completed) "On" else "Off"
+ )
+ },
+ secondaryLabel = if (task.steps.isNotEmpty()) {
+ {
+ Text("${task.steps.filter { it.completed }.size}/${task.steps.size}")
+ }
+ } else null,
+ )
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskListScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskListScreen.kt
new file mode 100644
index 00000000..14ef53bc
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskListScreen.kt
@@ -0,0 +1,66 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.list
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.ScalingLazyColumn
+import androidx.wear.compose.material.items
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.destinations.sample.wear.core.viewmodel.viewModel
+import com.ramcosta.destinations.sample.wear.destinations.AccountScreenDestination
+import com.ramcosta.destinations.sample.wear.destinations.AddTaskScreenDestination
+import com.ramcosta.destinations.sample.wear.destinations.SettingsScreenDestination
+import com.ramcosta.destinations.sample.wear.destinations.TaskScreenDestination
+
+@RootNavGraph(start = true)
+@Destination
+@Composable
+fun TaskListScreen(
+ navigator: DestinationsNavigator,
+ viewModel: TaskListViewModel = viewModel()
+) {
+ val tasks by viewModel.tasks.collectAsState()
+
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ Row {
+ Button(
+ onClick = { navigator.navigate(AddTaskScreenDestination) }) {
+ Icon(imageVector = Icons.Default.Add, contentDescription = "add task button")
+ }
+
+ Button(onClick = { navigator.navigate(AccountScreenDestination) }) {
+ Icon(imageVector = Icons.Default.AccountCircle, contentDescription = "Account")
+ }
+
+ Button(onClick = { navigator.navigate(SettingsScreenDestination) }) {
+ Icon(imageVector = Icons.Default.Settings, contentDescription = "Settings")
+ }
+ }
+ }
+ items(tasks) { item ->
+ TaskItem(
+ task = item,
+ onCheckedChange = {
+ viewModel.onCheckboxChecked(item.task, it)
+ },
+ onTaskClicked = {
+ navigator.navigate(TaskScreenDestination(item.task.id))
+ }
+ )
+ }
+ }
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskListViewModel.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskListViewModel.kt
new file mode 100644
index 00000000..0dd2dbfd
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskListViewModel.kt
@@ -0,0 +1,29 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.list
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.destinations.sample.wear.tasks.data.StepsRepository
+import com.ramcosta.destinations.sample.wear.tasks.data.TasksRepository
+import com.ramcosta.destinations.sample.wear.tasks.domain.Task
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+
+class TaskListViewModel(
+ private val tasksRepository: TasksRepository,
+ private val stepsRepository: StepsRepository
+) : ViewModel() {
+
+ val tasks: StateFlow> = tasksRepository.tasks
+ .combine(stepsRepository.steps) { tasks, steps ->
+ tasks.map { task ->
+ TaskUiItem(task, steps.filter { it.taskId == task.id })
+ }
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ fun onCheckboxChecked(task: Task, checked: Boolean) {
+ viewModelScope.launch {
+ tasksRepository.updateTask(task.copy(completed = checked))
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskUiItem.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskUiItem.kt
new file mode 100644
index 00000000..03cd4446
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/list/TaskUiItem.kt
@@ -0,0 +1,9 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.list
+
+import com.ramcosta.destinations.sample.wear.tasks.domain.Step
+import com.ramcosta.destinations.sample.wear.tasks.domain.Task
+
+data class TaskUiItem(
+ val task: Task,
+ val steps: List
+)
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddStepScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddStepScreen.kt
new file mode 100644
index 00000000..511de27e
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddStepScreen.kt
@@ -0,0 +1,30 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.new
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.destinations.sample.wear.core.viewmodel.viewModel
+import com.ramcosta.destinations.sample.wear.ui.composables.TitleConfirmDialog
+
+data class AddStepDialogNavArgs(
+ val taskId: Int
+)
+
+@Destination(navArgsDelegate = AddStepDialogNavArgs::class)
+@Composable
+fun AddStepScreen(
+ navigator: DestinationsNavigator,
+ viewModel: AddStepViewModel = viewModel()
+) {
+ TitleConfirmDialog(
+ type = "step", //use string resources in a real app ofc :)
+ title = viewModel.title.collectAsState().value,
+ onTitleChange = viewModel::onTitleChange,
+ onConfirm = {
+ viewModel.onConfirmClicked()
+ navigator.popBackStack()
+ },
+ onDismissRequest = { navigator.popBackStack() }
+ )
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddStepViewModel.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddStepViewModel.kt
new file mode 100644
index 00000000..17d65cc8
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddStepViewModel.kt
@@ -0,0 +1,29 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.new
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.destinations.sample.wear.tasks.data.StepsRepository
+import com.ramcosta.destinations.sample.wear.tasks.domain.Step
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class AddStepViewModel(
+ private val taskId: Int,
+ private val stepsRepository: StepsRepository
+) : ViewModel() {
+
+ private val _title = MutableStateFlow("")
+ val title = _title.asStateFlow()
+
+ fun onTitleChange(newTitle: String) {
+ _title.update { newTitle }
+ }
+
+ fun onConfirmClicked() {
+ viewModelScope.launch {
+ stepsRepository.addNewStep(Step(taskId, _title.value, false))
+ }
+ }
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddTaskScreen.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddTaskScreen.kt
new file mode 100644
index 00000000..421fdb31
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddTaskScreen.kt
@@ -0,0 +1,26 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.new
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.destinations.sample.wear.core.viewmodel.viewModel
+import com.ramcosta.destinations.sample.wear.ui.composables.TitleConfirmDialog
+
+@Destination
+@Composable
+fun AddTaskScreen(
+ navigator: DestinationsNavigator,
+ viewModel: AddTaskViewModel = viewModel()
+) {
+ TitleConfirmDialog(
+ type = "task", //use string resources in a real app ofc :)
+ title = viewModel.title.collectAsState().value,
+ onTitleChange = viewModel::onTitleChange,
+ onConfirm = {
+ viewModel.onConfirmClicked()
+ navigator.popBackStack()
+ },
+ onDismissRequest = { navigator.popBackStack() }
+ )
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddTaskViewModel.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddTaskViewModel.kt
new file mode 100644
index 00000000..0acb791b
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/tasks/presentation/new/AddTaskViewModel.kt
@@ -0,0 +1,28 @@
+package com.ramcosta.destinations.sample.wear.tasks.presentation.new
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.destinations.sample.wear.tasks.data.TasksRepository
+import com.ramcosta.destinations.sample.wear.tasks.domain.Task
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class AddTaskViewModel(
+ private val tasksRepository: TasksRepository
+) : ViewModel() {
+
+ private val _title = MutableStateFlow("")
+ val title = _title.asStateFlow()
+
+ fun onTitleChange(newTitle: String) {
+ _title.update { newTitle }
+ }
+
+ fun onConfirmClicked() {
+ viewModelScope.launch {
+ tasksRepository.addNewTask(Task(_title.value, "", false))
+ }
+ }
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/composables/SampleScaffold.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/composables/SampleScaffold.kt
new file mode 100644
index 00000000..ffb2e2ef
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/composables/SampleScaffold.kt
@@ -0,0 +1,29 @@
+package com.ramcosta.destinations.sample.wear.ui.composables
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavHostController
+import androidx.wear.compose.material.Scaffold
+import androidx.wear.compose.material.TimeText
+import com.ramcosta.composedestinations.spec.Route
+import com.ramcosta.destinations.sample.wear.appCurrentDestinationAsState
+import com.ramcosta.destinations.sample.wear.shouldShowScaffoldElements
+import com.ramcosta.destinations.sample.wear.startAppDestination
+
+@Composable
+fun SampleScaffold(
+ startRoute: Route,
+ navController: NavHostController,
+ content: @Composable () -> Unit,
+) {
+ val destination = navController.appCurrentDestinationAsState().value
+ ?: startRoute.startAppDestination
+
+ Scaffold(
+ timeText = {
+ if (destination.shouldShowScaffoldElements) {
+ TimeText()
+ }
+ },
+ content = content
+ )
+}
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/composables/TitleConfirmDialog.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/composables/TitleConfirmDialog.kt
new file mode 100644
index 00000000..77a90252
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/composables/TitleConfirmDialog.kt
@@ -0,0 +1,87 @@
+package com.ramcosta.destinations.sample.wear.ui.composables
+
+import android.app.RemoteInput
+import android.os.Bundle
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.dialog.Dialog
+import androidx.wear.input.RemoteInputIntentHelper
+
+@Composable
+fun TitleConfirmDialog(
+ type: String,
+ title: String,
+ onTitleChange: (String) -> Unit,
+ onConfirm: () -> Unit,
+ onDismissRequest: () -> Unit
+) {
+ Dialog(showDialog = true, onDismissRequest = onDismissRequest) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceAround
+ ) {
+ var textForUserInput by remember { mutableStateOf("") }
+ val inputTextKey = "input_text"
+
+ val launcher =
+ rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ it.data?.let { data ->
+ val results: Bundle = RemoteInput.getResultsFromIntent(data)
+ val newInputText: CharSequence? = results.getCharSequence(inputTextKey)
+ textForUserInput = newInputText as String
+ }
+ }
+
+ val intent = remember {
+ RemoteInputIntentHelper.createActionRemoteInputIntent().apply {
+ RemoteInputIntentHelper.putRemoteInputsExtra(
+ this, listOf(
+ RemoteInput.Builder(inputTextKey)
+ .setLabel("${type.replaceFirstChar { it.uppercase() }} title")
+ .build()
+ )
+ )
+ }
+ }
+
+ Text("Add a new $type:")
+
+ Text(
+ modifier = Modifier.fillMaxWidth(0.8f).border(1.dp, Color.White).clickable {
+ launcher.launch(intent)
+ },
+ text = textForUserInput,
+ )
+
+ Chip(
+ onClick = {
+ onTitleChange(textForUserInput)
+ onConfirm()
+ },
+ label = {
+ Text("Confirm")
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/theme/Theme.kt b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/theme/Theme.kt
new file mode 100644
index 00000000..42ec02aa
--- /dev/null
+++ b/sample-wear/src/main/java/com/ramcosta/destinations/sample/wear/ui/theme/Theme.kt
@@ -0,0 +1,13 @@
+package com.ramcosta.destinations.sample.wear.ui.theme
+
+import androidx.compose.runtime.Composable
+import androidx.wear.compose.material.MaterialTheme
+
+@Composable
+fun DestinationsTodoSampleTheme(
+ content: @Composable () -> Unit
+) {
+ MaterialTheme(
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/sample-wear/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample-wear/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/sample-wear/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-wear/src/main/res/drawable/ic_launcher_background.xml b/sample-wear/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/sample-wear/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample-wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample-wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/sample-wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample-wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/sample-wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-wear/src/main/res/mipmap-hdpi/ic_launcher.webp b/sample-wear/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/sample-wear/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/sample-wear/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/sample-wear/src/main/res/mipmap-mdpi/ic_launcher.webp b/sample-wear/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/sample-wear/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/sample-wear/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/sample-wear/src/main/res/mipmap-xhdpi/ic_launcher.webp b/sample-wear/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/sample-wear/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/sample-wear/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/sample-wear/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/sample-wear/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/sample-wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/sample-wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/sample-wear/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/sample-wear/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/sample-wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/sample-wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/sample-wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/sample-wear/src/main/res/values/strings.xml b/sample-wear/src/main/res/values/strings.xml
new file mode 100644
index 00000000..7f8070a1
--- /dev/null
+++ b/sample-wear/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+ sample
+ Settings
+ Account
+ Tasks
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2f1067b2..938c19eb 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -12,6 +12,8 @@ include(":compose-destinations")
include(":compose-destinations-ksp")
include(":compose-destinations-codegen")
include(":compose-destinations-animations")
+include(":compose-destinations-wear")
include(":sample")
+include(":sample-wear")
include(":playground")
-include(":playground-shared")
+include(":playground-core")