From 89507d56181800f4e3621dcaadbfc9bd0541b154 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Mon, 19 May 2025 17:15:24 -0700 Subject: [PATCH 1/5] Add navigation3 snippets --- compose/snippets/build.gradle.kts | 7 + .../compose/snippets/navigation3/Content.kt | 185 ++++++++++++++++ .../animations/AnimationSnippets.kt | 131 +++++++++++ .../navigation3/basic/BasicSnippets.kt | 124 +++++++++++ .../savingstate/SavingStateSnippets.kt | 60 +++++ .../navigation3/scenes/ScenesSnippets.kt | 206 ++++++++++++++++++ .../scenes/material/MaterialScenesSnippets.kt | 111 ++++++++++ .../compose/snippets/ui/theme/Color.kt | 10 + gradle/libs.versions.toml | 14 +- 9 files changed, 847 insertions(+), 1 deletion(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index c8d728da0..8f8f8a79b 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -95,6 +95,7 @@ dependencies { implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.navigation) implementation(libs.androidx.compose.material3.adaptive.navigation.suite) + implementation(libs.androidx.compose.material3.windowsizeclass) implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.runtime) @@ -136,7 +137,13 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) + + implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.compose.material3.adaptive.navigation3) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.recyclerview) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt new file mode 100644 index 000000000..1fd2d3039 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation3 + +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.ui.theme.PastelBlue +import com.example.compose.snippets.ui.theme.PastelGreen +import com.example.compose.snippets.ui.theme.PastelMauve +import com.example.compose.snippets.ui.theme.PastelOrange +import com.example.compose.snippets.ui.theme.PastelPink +import com.example.compose.snippets.ui.theme.PastelPurple +import com.example.compose.snippets.ui.theme.PastelRed +import com.example.compose.snippets.ui.theme.PastelYellow + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun ContentBase( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .clip(RoundedCornerShape(48.dp)) + ) { + Title(title) + if (content != null) content() + if (onNext != null) { + Button( + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = onNext + ) { + Text("Next") + } + } + } + +} + +@Composable +fun ColumnScope.Title(title: String) { + Text( + modifier = Modifier + .padding(24.dp) + .align(Alignment.CenterHorizontally), + fontWeight = FontWeight.Bold, + text = title + ) +} + +@Composable +fun ContentRed( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelRed), + onNext = onNext, + content = content +) + +@Composable +fun ContentOrange( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelOrange), + onNext = onNext, + content = content +) + +@Composable +fun ContentYellow( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelYellow), + onNext = onNext, + content = content +) + +@Composable +fun ContentGreen( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelGreen), + onNext = onNext, + content = content +) + +@Composable +fun ContentBlue( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelBlue), + onNext = onNext, + content = content +) + +@Composable +fun ContentMauve( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelMauve), + onNext = onNext, + content = content +) + +@Composable +fun ContentPurple( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelPurple), + onNext = onNext, + content = content +) + +@Composable +fun ContentPink( + title: String, + modifier: Modifier = Modifier, + onNext: (() -> Unit)? = null, + content: (@Composable () -> Unit)? = null, +) = ContentBase( + title = title, + modifier = modifier.background(PastelPink), + onNext = onNext, + content = content +) \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt new file mode 100644 index 000000000..e77a391d0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation3.animations + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.compose.snippets.navigation3.ContentGreen +import com.example.compose.snippets.navigation3.ContentMauve +import com.example.compose.snippets.navigation3.ContentOrange +import kotlinx.serialization.Serializable + +// [START android_compose_navigation3_animations_1] +@Serializable +data object ScreenA : NavKey + +@Serializable +data object ScreenB : NavKey + +@Serializable +data object ScreenC : NavKey + + +class AnimatedNavDisplayActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + + Scaffold { paddingValues -> + + val backStack = rememberNavBackStack(ScreenA) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { + ContentOrange("This is Screen A") { + Button(onClick = { backStack.add(ScreenB) }) { + Text("Go to Screen B") + } + } + } + entry { + ContentMauve("This is Screen B") { + Button(onClick = { backStack.add(ScreenC) }) { + Text("Go to Screen C") + } + } + } + entry( + metadata = NavDisplay.transitionSpec { + // Slide new content up, keeping the old content in place underneath + slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(1000) + ) togetherWith ExitTransition.KeepUntilTransitionsFinished + } + NavDisplay.popTransitionSpec { + // Slide old content down, revealing the new content in place underneath + EnterTransition.None togetherWith + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(1000) + ) + } + NavDisplay.predictivePopTransitionSpec { + // Slide old content down, revealing the new content in place underneath + EnterTransition.None togetherWith + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(1000) + ) + } + ){ + ContentGreen("This is Screen C") + } + }, + transitionSpec = { + // Slide in from right when navigating forward + slideInHorizontally(initialOffsetX = { it }) togetherWith + slideOutHorizontally(targetOffsetX = { -it }) + }, + popTransitionSpec = { + // Slide in from left when navigating back + slideInHorizontally(initialOffsetX = { -it }) togetherWith + slideOutHorizontally(targetOffsetX = { it }) + }, + predictivePopTransitionSpec = { + // Slide in from left when navigating back + slideInHorizontally(initialOffsetX = { -it }) togetherWith + slideOutHorizontally(targetOffsetX = { it }) + }, + modifier = Modifier.padding(paddingValues) + ) + } + } + } +} +// [END android_compose_navigation3_animations_1] \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt new file mode 100644 index 000000000..d45b8502d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation3.basic + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.compose.snippets.navigation3.savingstate.Home + +// [START android_compose_navigation3_basic_1] +// Define keys that will identify content +data object ProductList +data class ProductDetail(val id: String) + +@Composable +fun MyApp() { + + // Create a back stack, specifying the key the app should start with + val backStack = remember { mutableStateListOf(ProductList) } + + // Supply your back stack to a NavDisplay so it can reflect changes in the UI + //...more on this below... + + // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state + backStack.add(ProductDetail(id = "ABC")) + + // Pop a key off the back stack (navigate back), the navigation library will reflect the change in state + backStack.removeLastOrNull() +} +// [END android_compose_navigation3_basic_1] + +@Composable +fun EntryProvider() { + val backStack = remember { mutableStateListOf(ProductList) } + NavDisplay( + backStack = backStack, + // [START android_compose_navigation3_basic_2] + entryProvider = { key -> + when (key) { + is ProductList -> NavEntry(key) { Text("Product List") } + is ProductDetail -> NavEntry( + key, + metadata = mapOf("extraDataKey" to "extraDataValue") + ) { Text("Product ${key.id} ") } + + else -> { + NavEntry(Unit) { Text(text = "Invalid Key: $it") } + } + } + } + // [END android_compose_navigation3_basic_2] + ) +} + +@Composable +fun EntryProviderDsl() { + val backStack = remember { mutableStateListOf(ProductList) } + NavDisplay( + backStack = backStack, + // [START android_compose_navigation3_basic_3] + entryProvider = entryProvider { + entry { Text("Product List") } + entry( + metadata = mapOf("extraDataKey" to "extraDataValue") + ) { key -> Text("Product ${key.id} ") } + } + // [END android_compose_navigation3_basic_3] + ) +} + +// [START android_compose_navigation3_basic_4] +data object Home +data class Product(val id: String) + +@Composable +fun NavExample() { + + val backStack = remember { mutableStateListOf(Home) } + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = { key -> + when (key) { + is Home -> NavEntry(key) { + _root_ide_package_.com.example.compose.snippets.navigation3.ContentGreen("Welcome to Nav3") { + Button(onClick = { + backStack.add(Product("123")) + }) { + Text("Click to navigate") + } + } + } + + is Product -> NavEntry(key) { + _root_ide_package_.com.example.compose.snippets.navigation3.ContentBlue("Product ${key.id} ") + } + + else -> NavEntry(Unit) { Text("Unknown route") } + } + } + ) +} +// [END android_compose_navigation3_basic_4] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt new file mode 100644 index 000000000..0fd2778e3 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation3.savingstate + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator +import kotlinx.serialization.Serializable +import java.util.Map.entry + +// [START android_compose_navigation3_savingstate_1] +@Serializable +data object Home : NavKey + +@Composable +fun NavBackStack() { + val backStack = rememberNavBackStack(Home) +} +// [END android_compose_navigation3_savingstate_1] + +@Composable +fun ScopingViewModels() { + + val backStack = rememberNavBackStack(Home) + + // [START android_compose_navigation3_savingstate_2] + NavDisplay( + entryDecorators = listOf( + // Add the default decorators for managing scenes and saving state + rememberSceneSetupNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), + // Then add the view model store decorator + rememberViewModelStoreNavEntryDecorator() + ), + backStack = backStack, + entryProvider = entryProvider { }, + ) + // [END android_compose_navigation3_savingstate_2] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt new file mode 100644 index 000000000..368a467c0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation3.scenes + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.Scene +import androidx.navigation3.ui.SceneStrategy +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND +import kotlinx.serialization.Serializable + +interface SceneExample { + + // [START android_compose_navigation3_scenes_1] + @Composable + public fun calculateScene( + entries: List>, + onBack: (count: Int) -> Unit, + ): Scene? + // [END android_compose_navigation3_scenes_1] +} + +// [START android_compose_navigation3_scenes_2] +data class SinglePaneScene( + override val key: T, + val entry: NavEntry, + override val previousEntries: List>, +) : Scene { + override val entries: List> = listOf(entry) + override val content: @Composable () -> Unit = { entry.content.invoke(entry.key) } +} + +/** + * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the + * list. + */ +public class SinglePaneSceneStrategy : SceneStrategy { + @Composable + override fun calculateScene(entries: List>, onBack: (Int) -> Unit): Scene = + SinglePaneScene( + key = entries.last().key, + entry = entries.last(), + previousEntries = entries.dropLast(1) + ) +} +// [END android_compose_navigation3_scenes_2] + +// [START android_compose_navigation3_scenes_3] +// --- TwoPaneScene --- +/** + * A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split. + */ +class TwoPaneScene( + override val key: Any, + override val previousEntries: List>, + val firstEntry: NavEntry, + val secondEntry: NavEntry +) : Scene { + override val entries: List> = listOf(firstEntry, secondEntry) + override val content: @Composable (() -> Unit) = { + Row(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.weight(0.5f)) { + firstEntry.content.invoke(firstEntry.key) + } + Column(modifier = Modifier.weight(0.5f)) { + secondEntry.content.invoke(secondEntry.key) + } + } + } + + companion object { + internal const val TWO_PANE_KEY = "TwoPane" + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed + * in a two-pane layout. + */ + fun twoPane() = mapOf(TWO_PANE_KEY to true) + } +} + +// --- TwoPaneSceneStrategy --- +/** + * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough + * and the top two back stack entries declare support for two-pane display. + */ +class TwoPaneSceneStrategy : SceneStrategy { + @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class) + @Composable + override fun calculateScene( + entries: List>, + onBack: (Int) -> Unit + ): Scene? { + + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. + // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). + if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { + return null + } + + val lastTwoEntries = entries.takeLast(2) + + // Condition 2: Only return a Scene if there are two entries, and both have declared + // they can be displayed in a two pane scene. + return if (lastTwoEntries.size == 2 + && lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } + ) { + val firstEntry = lastTwoEntries.first() + val secondEntry = lastTwoEntries.last() + + // The scene key must uniquely represent the state of the scene. + val sceneKey = Pair(firstEntry.key, secondEntry.key) + + TwoPaneScene( + key = sceneKey, + // Where we go back to is a UX decision. In this case, we only remove the top + // entry from the back stack, despite displaying two entries in this scene. + // This is because in this app we only ever add one entry to the + // back stack at a time. It would therefore be confusing to the user to add one + // when navigating forward, but remove two when navigating back. + previousEntries = entries.dropLast(1), + firstEntry = firstEntry, + secondEntry = secondEntry + ) + } else { + null + } + } +} +// [END android_compose_navigation3_scenes_3] + +// [START android_compose_navigation3_scenes_4] +// Define your navigation keys +@Serializable +data object ProductList : NavKey +@Serializable +data class ProductDetail(val id: String) : NavKey + +@Composable +fun MyAppContent() { + val backStack = rememberNavBackStack(ProductList) + + NavDisplay( + backStack = backStack, + entryProvider = entryProvider { + entry( + // Mark this entry as eligible for two-pane display + metadata = TwoPaneScene.twoPane() + ) { key -> + Column { + Text("Product List") + Button(onClick = { backStack.add(ProductDetail("ABC")) }) { + Text("View Details for ABC (Two-Pane Eligible)") + } + } + } + + entry( + // Mark this entry as eligible for two-pane display + metadata = TwoPaneScene.twoPane() + ) { key -> + Text("Product Detail: ${key.id} (Two-Pane Eligible)") + } + // ... other entries ... + }, + // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically. + sceneStrategy = TwoPaneSceneStrategy(), + onBack = { count -> + repeat(count) { + if (backStack.isNotEmpty()) { + backStack.removeLastOrNull() + } + } + } + ) +} +// [END android_compose_navigation3_scenes_4] \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt new file mode 100644 index 000000000..011e8e677 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation3.scenes.material + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.compose.snippets.navigation3.ContentBlue +import com.example.compose.snippets.navigation3.ContentGreen +import com.example.compose.snippets.navigation3.ContentRed +import com.example.compose.snippets.navigation3.ContentYellow +import com.example.compose.snippets.ui.theme.PastelBlue +import kotlinx.serialization.Serializable + +// [START android_compose_navigation3_scenes_material_1] +@Serializable +object ProductList : NavKey + +@Serializable +data class ProductDetail(val id: String) : NavKey + +@Serializable +data object Profile : NavKey + +class MaterialListDetailActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Scaffold { paddingValues -> + val backStack = rememberNavBackStack(ProductList) + val listDetailStrategy = rememberListDetailSceneStrategy() + + NavDisplay( + backStack = backStack, + modifier = Modifier.padding(paddingValues), + onBack = { keysToRemove -> repeat(keysToRemove){ backStack.removeLastOrNull() } }, + sceneStrategy = listDetailStrategy, + entryProvider = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane( + detailPlaceholder = { + ContentYellow("Choose a product from the list") + } + ) + ) { + ContentRed("Welcome to Nav3"){ + Button(onClick = { + backStack.add(ProductDetail("ABC")) + } ) { + Text("View product") + } + } + } + entry( + metadata = ListDetailSceneStrategy.detailPane() + ) { product -> + ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = { + backStack.add(Profile) + }) { + Text("View profile") + } + } + } + } + entry( + metadata = ListDetailSceneStrategy.extraPane() + ) { + ContentGreen("Profile") + } + } + ) + } + } + } +} +// [END android_compose_navigation3_scenes_material_1] \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt index 6c6006ead..9c93de9cc 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt @@ -31,3 +31,13 @@ val LavenderLight = Color(0xFFDDBEFC) val RoseDark = Color(0xffaf0060) val RoseLight = Color(0xFFFFAFC9) + +val PastelRed = Color(0xFFFFADAD) +val PastelOrange = Color(0xFFFFD6A5) +val PastelYellow = Color(0xFFFDFFB6) +val PastelGreen = Color(0xFFCAFFBF) +val PastelBlue = Color(0xFF9BF6FF) +val PastelMauve = Color(0xFFA0C4FF) +val PastelPurple = Color(0xFFBDB2FF) +val PastelPink = Color(0xFFFFC6FF) + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bab1c791..f2e5f73d3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,9 @@ androidx-credentials-play-services-auth = "1.5.0" androidx-emoji2-views = "1.5.0" androidx-fragment-ktx = "1.8.9" androidx-glance-appwidget = "1.1.1" +androidx-lifecycle-viewmodel-navigation3 = "1.0.0-SNAPSHOT" +androidx-navigation = "2.8.9" +androidx-navigation3 = "1.0.0-SNAPSHOT" androidx-lifecycle-compose = "2.9.2" androidx-lifecycle-runtime-compose = "2.9.2" androidx-navigation = "2.9.3" @@ -51,12 +54,14 @@ kotlin = "2.2.10" kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" kotlinxSerializationJson = "1.9.0" +kotlinxSerializationCore = "1.8.1" ksp = "2.2.10-2.0.2" lifecycleService = "2.9.2" maps-compose = "6.7.2" material = "1.14.0-alpha03" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" +material3-adaptive-navigation3 = "1.0.0-SNAPSHOT" media3 = "1.8.0" # @keep minSdk = "35" @@ -77,6 +82,7 @@ wearComposeMaterial3 = "1.5.0-rc02" wearOngoing = "1.0.0" wearToolingPreview = "1.0.0" webkit = "1.14.0" +material3WindowSizeClassAndroid = "1.3.2" [libraries] accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3" @@ -100,6 +106,8 @@ androidx-compose-material3-adaptive = { module = "androidx.compose.material3.ada androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" } +androidx-compose-material3-adaptive-navigation3 = { module = "androidx.compose.material3.adaptive:adaptive-navigation3", version.ref = "material3-adaptive-navigation3" } +androidx-compose-material3-windowsizeclass = { group = "androidx.compose.material3", name = "material3-window-size-class-android", version.ref = "material3-adaptive-navigation-suite" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-latest" } @@ -132,10 +140,13 @@ androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle-viewmodel-navigation3" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } androidx-protolayout = { module = "androidx.wear.protolayout:protolayout", version.ref = "protolayout" } androidx-protolayout-expression = { module = "androidx.wear.protolayout:protolayout-expression", version.ref = "protolayout" } @@ -184,7 +195,8 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = " kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } validator-push = { module = "com.google.android.wearable.watchface.validator:validator-push", version.ref = "validatorPush" } From c9405a16d7aff2cc857696b2b4d7689dc4a46631 Mon Sep 17 00:00:00 2001 From: dturner <873212+dturner@users.noreply.github.com> Date: Tue, 20 May 2025 00:19:12 +0000 Subject: [PATCH 2/5] Apply Spotless --- .../compose/snippets/navigation3/Content.kt | 6 ++--- .../animations/AnimationSnippets.kt | 27 +++++++++---------- .../navigation3/basic/BasicSnippets.kt | 2 +- .../savingstate/SavingStateSnippets.kt | 3 --- .../navigation3/scenes/ScenesSnippets.kt | 6 ++--- .../scenes/material/MaterialScenesSnippets.kt | 8 +++--- .../compose/snippets/ui/theme/Color.kt | 1 - 7 files changed, 23 insertions(+), 30 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt index 1fd2d3039..c96dff1e9 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -29,7 +29,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.example.compose.snippets.ui.theme.PastelBlue @@ -66,7 +65,6 @@ fun ContentBase( } } } - } @Composable @@ -182,4 +180,4 @@ fun ContentPink( modifier = modifier.background(PastelPink), onNext = onNext, content = content -) \ No newline at end of file +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt index e77a391d0..4def0c40b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt @@ -52,7 +52,6 @@ data object ScreenB : NavKey @Serializable data object ScreenC : NavKey - class AnimatedNavDisplayActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -91,36 +90,36 @@ class AnimatedNavDisplayActivity : ComponentActivity() { } + NavDisplay.popTransitionSpec { // Slide old content down, revealing the new content in place underneath EnterTransition.None togetherWith - slideOutVertically( - targetOffsetY = { it }, - animationSpec = tween(1000) - ) + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(1000) + ) } + NavDisplay.predictivePopTransitionSpec { // Slide old content down, revealing the new content in place underneath EnterTransition.None togetherWith - slideOutVertically( - targetOffsetY = { it }, - animationSpec = tween(1000) - ) + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(1000) + ) } - ){ + ) { ContentGreen("This is Screen C") } }, transitionSpec = { // Slide in from right when navigating forward slideInHorizontally(initialOffsetX = { it }) togetherWith - slideOutHorizontally(targetOffsetX = { -it }) + slideOutHorizontally(targetOffsetX = { -it }) }, popTransitionSpec = { // Slide in from left when navigating back slideInHorizontally(initialOffsetX = { -it }) togetherWith - slideOutHorizontally(targetOffsetX = { it }) + slideOutHorizontally(targetOffsetX = { it }) }, predictivePopTransitionSpec = { // Slide in from left when navigating back slideInHorizontally(initialOffsetX = { -it }) togetherWith - slideOutHorizontally(targetOffsetX = { it }) + slideOutHorizontally(targetOffsetX = { it }) }, modifier = Modifier.padding(paddingValues) ) @@ -128,4 +127,4 @@ class AnimatedNavDisplayActivity : ComponentActivity() { } } } -// [END android_compose_navigation3_animations_1] \ No newline at end of file +// [END android_compose_navigation3_animations_1] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt index d45b8502d..9e1a8afa1 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt @@ -39,7 +39,7 @@ fun MyApp() { val backStack = remember { mutableStateListOf(ProductList) } // Supply your back stack to a NavDisplay so it can reflect changes in the UI - //...more on this below... + // ...more on this below... // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state backStack.add(ProductDetail(id = "ABC")) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt index 0fd2778e3..733191e39 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt @@ -16,18 +16,15 @@ package com.example.compose.snippets.navigation3.savingstate -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import kotlinx.serialization.Serializable -import java.util.Map.entry // [START android_compose_navigation3_savingstate_1] @Serializable diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt index 368a467c0..9ec3508c8 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt @@ -131,8 +131,8 @@ class TwoPaneSceneStrategy : SceneStrategy { // Condition 2: Only return a Scene if there are two entries, and both have declared // they can be displayed in a two pane scene. - return if (lastTwoEntries.size == 2 - && lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } + return if (lastTwoEntries.size == 2 && + lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } ) { val firstEntry = lastTwoEntries.first() val secondEntry = lastTwoEntries.last() @@ -203,4 +203,4 @@ fun MyAppContent() { } ) } -// [END android_compose_navigation3_scenes_4] \ No newline at end of file +// [END android_compose_navigation3_scenes_4] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt index 011e8e677..d71ab3f6b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt @@ -66,7 +66,7 @@ class MaterialListDetailActivity : ComponentActivity() { NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), - onBack = { keysToRemove -> repeat(keysToRemove){ backStack.removeLastOrNull() } }, + onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, sceneStrategy = listDetailStrategy, entryProvider = entryProvider { entry( @@ -76,10 +76,10 @@ class MaterialListDetailActivity : ComponentActivity() { } ) ) { - ContentRed("Welcome to Nav3"){ + ContentRed("Welcome to Nav3") { Button(onClick = { backStack.add(ProductDetail("ABC")) - } ) { + }) { Text("View product") } } @@ -108,4 +108,4 @@ class MaterialListDetailActivity : ComponentActivity() { } } } -// [END android_compose_navigation3_scenes_material_1] \ No newline at end of file +// [END android_compose_navigation3_scenes_material_1] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt index 9c93de9cc..056b960dd 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt @@ -40,4 +40,3 @@ val PastelBlue = Color(0xFF9BF6FF) val PastelMauve = Color(0xFFA0C4FF) val PastelPurple = Color(0xFFBDB2FF) val PastelPink = Color(0xFFFFC6FF) - From 9105e569b366e2a471cca1c8cd3841cba478af43 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Tue, 20 May 2025 11:18:48 -0700 Subject: [PATCH 3/5] Remove long package name --- .../compose/snippets/navigation3/basic/BasicSnippets.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt index 9e1a8afa1..38d76b123 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt @@ -25,6 +25,8 @@ import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay +import com.example.compose.snippets.navigation3.ContentBlue +import com.example.compose.snippets.navigation3.ContentGreen import com.example.compose.snippets.navigation3.savingstate.Home // [START android_compose_navigation3_basic_1] @@ -103,7 +105,7 @@ fun NavExample() { entryProvider = { key -> when (key) { is Home -> NavEntry(key) { - _root_ide_package_.com.example.compose.snippets.navigation3.ContentGreen("Welcome to Nav3") { + ContentGreen("Welcome to Nav3") { Button(onClick = { backStack.add(Product("123")) }) { @@ -113,7 +115,7 @@ fun NavExample() { } is Product -> NavEntry(key) { - _root_ide_package_.com.example.compose.snippets.navigation3.ContentBlue("Product ${key.id} ") + ContentBlue("Product ${key.id} ") } else -> NavEntry(Unit) { Text("Unknown route") } From e79944624dbe60929e307963cc7036898973ab6f Mon Sep 17 00:00:00 2001 From: Don Turner Date: Tue, 23 Sep 2025 10:02:26 +0100 Subject: [PATCH 4/5] Update Nav3 snippets to match alpha09 API --- .../snippets/navigation3/scenes/ScenesSnippets.kt | 12 ++++++------ gradle/libs.versions.toml | 9 ++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt index 9ec3508c8..548a385d4 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt @@ -50,12 +50,12 @@ interface SceneExample { // [START android_compose_navigation3_scenes_2] data class SinglePaneScene( - override val key: T, + override val key: Any, val entry: NavEntry, override val previousEntries: List>, ) : Scene { override val entries: List> = listOf(entry) - override val content: @Composable () -> Unit = { entry.content.invoke(entry.key) } + override val content: @Composable () -> Unit = { entry.Content() } } /** @@ -66,7 +66,7 @@ public class SinglePaneSceneStrategy : SceneStrategy { @Composable override fun calculateScene(entries: List>, onBack: (Int) -> Unit): Scene = SinglePaneScene( - key = entries.last().key, + key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) @@ -88,10 +88,10 @@ class TwoPaneScene( override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.5f)) { - firstEntry.content.invoke(firstEntry.key) + firstEntry.Content() } Column(modifier = Modifier.weight(0.5f)) { - secondEntry.content.invoke(secondEntry.key) + secondEntry.Content() } } } @@ -138,7 +138,7 @@ class TwoPaneSceneStrategy : SceneStrategy { val secondEntry = lastTwoEntries.last() // The scene key must uniquely represent the state of the scene. - val sceneKey = Pair(firstEntry.key, secondEntry.key) + val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey) TwoPaneScene( key = sceneKey, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2e5f73d3..016c4f791 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,9 +17,8 @@ androidx-credentials-play-services-auth = "1.5.0" androidx-emoji2-views = "1.5.0" androidx-fragment-ktx = "1.8.9" androidx-glance-appwidget = "1.1.1" -androidx-lifecycle-viewmodel-navigation3 = "1.0.0-SNAPSHOT" -androidx-navigation = "2.8.9" -androidx-navigation3 = "1.0.0-SNAPSHOT" +androidx-lifecycle-viewmodel-navigation3 = "1.0.0-alpha04" +androidx-navigation3 = "1.0.0-alpha09" androidx-lifecycle-compose = "2.9.2" androidx-lifecycle-runtime-compose = "2.9.2" androidx-navigation = "2.9.3" @@ -34,7 +33,7 @@ androidx-window-java = "1.5.0-beta02" androidx-xr-arcore = "1.0.0-alpha05" androidx-xr-compose = "1.0.0-alpha06" androidx-xr-scenecore = "1.0.0-alpha06" -androidxHiltNavigationCompose = "1.2.0" +androidxHiltNavigationCompose = "1.3.0" appcompat = "1.7.1" coil = "2.7.0" # @keep @@ -61,7 +60,7 @@ maps-compose = "6.7.2" material = "1.14.0-alpha03" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" -material3-adaptive-navigation3 = "1.0.0-SNAPSHOT" +material3-adaptive-navigation3 = "1.0.0-alpha02" media3 = "1.8.0" # @keep minSdk = "35" From cfce7636a39f4c8a193b1269da736b8f46893b0b Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia <43899917+srikrishnasakunia@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:57:00 +0000 Subject: [PATCH 5/5] Update Nav3 to beta01 (#672) * Update Nav3 to beta01 * Apply Spotless --- .../animations/AnimationSnippets.kt | 1 - .../navigation3/basic/BasicSnippets.kt | 1 - .../savingstate/SavingStateSnippets.kt | 6 +-- .../navigation3/scenes/ScenesSnippets.kt | 44 +++++++++---------- .../scenes/material/MaterialScenesSnippets.kt | 5 +-- gradle/libs.versions.toml | 10 ++--- 6 files changed, 30 insertions(+), 37 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt index 4def0c40b..2a6dcc036 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt @@ -33,7 +33,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.ui.Modifier import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt index 38d76b123..bedb20aa7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.example.compose.snippets.navigation3.ContentBlue diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt index 733191e39..48179f68d 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt @@ -21,9 +21,8 @@ import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDe import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay -import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import kotlinx.serialization.Serializable // [START android_compose_navigation3_savingstate_1] @@ -45,8 +44,7 @@ fun ScopingViewModels() { NavDisplay( entryDecorators = listOf( // Add the default decorators for managing scenes and saving state - rememberSceneSetupNavEntryDecorator(), - rememberSavedStateNavEntryDecorator(), + rememberSaveableStateHolderNavEntryDecorator(), // Then add the view model store decorator rememberViewModelStoreNavEntryDecorator() ), diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt index 548a385d4..ab160b1d4 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt @@ -21,19 +21,19 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.scene.Scene +import androidx.navigation3.scene.SceneStrategy +import androidx.navigation3.scene.SceneStrategyScope import androidx.navigation3.ui.NavDisplay -import androidx.navigation3.ui.Scene -import androidx.navigation3.ui.SceneStrategy +import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND import kotlinx.serialization.Serializable @@ -63,8 +63,7 @@ data class SinglePaneScene( * list. */ public class SinglePaneSceneStrategy : SceneStrategy { - @Composable - override fun calculateScene(entries: List>, onBack: (Int) -> Unit): Scene = + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), @@ -106,21 +105,22 @@ class TwoPaneScene( } } +@Composable +fun rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + return remember(windowSizeClass) { + TwoPaneSceneStrategy(windowSizeClass) + } +} + // --- TwoPaneSceneStrategy --- /** * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough * and the top two back stack entries declare support for two-pane display. */ -class TwoPaneSceneStrategy : SceneStrategy { - @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class) - @Composable - override fun calculateScene( - entries: List>, - onBack: (Int) -> Unit - ): Scene? { - - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - +class TwoPaneSceneStrategy(val windowSizeClass: WindowSizeClass) : SceneStrategy { + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { @@ -193,12 +193,10 @@ fun MyAppContent() { // ... other entries ... }, // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically. - sceneStrategy = TwoPaneSceneStrategy(), - onBack = { count -> - repeat(count) { - if (backStack.isNotEmpty()) { - backStack.removeLastOrNull() - } + sceneStrategy = rememberTwoPaneSceneStrategy(), + onBack = { + if (backStack.isNotEmpty()) { + backStack.removeLastOrNull() } } ) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt index d71ab3f6b..e47a4ab2c 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneSt import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay @@ -61,12 +60,12 @@ class MaterialListDetailActivity : ComponentActivity() { setContent { Scaffold { paddingValues -> val backStack = rememberNavBackStack(ProductList) - val listDetailStrategy = rememberListDetailSceneStrategy() + val listDetailStrategy = rememberListDetailSceneStrategy() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), - onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, + onBack = { backStack.removeLastOrNull() }, sceneStrategy = listDetailStrategy, entryProvider = entryProvider { entry( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 016c4f791..c47fdfda6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,10 +17,10 @@ androidx-credentials-play-services-auth = "1.5.0" androidx-emoji2-views = "1.5.0" androidx-fragment-ktx = "1.8.9" androidx-glance-appwidget = "1.1.1" -androidx-lifecycle-viewmodel-navigation3 = "1.0.0-alpha04" -androidx-navigation3 = "1.0.0-alpha09" -androidx-lifecycle-compose = "2.9.2" -androidx-lifecycle-runtime-compose = "2.9.2" +androidx-lifecycle-viewmodel-navigation3 = "2.10.0-beta01" +androidx-navigation3 = "1.0.0-beta01" +androidx-lifecycle-compose = "2.10.0-beta01" +androidx-lifecycle-runtime-compose = "2.10.0-beta01" androidx-navigation = "2.9.3" androidx-paging = "3.3.6" androidx-startup-runtime = "1.2.0" @@ -60,7 +60,7 @@ maps-compose = "6.7.2" material = "1.14.0-alpha03" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" -material3-adaptive-navigation3 = "1.0.0-alpha02" +material3-adaptive-navigation3 = "1.3.0-alpha01" media3 = "1.8.0" # @keep minSdk = "35"