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..c96dff1e9 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/Content.kt @@ -0,0 +1,183 @@ +/* + * 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 + +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.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 +) 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..2a6dcc036 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt @@ -0,0 +1,129 @@ +/* + * 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.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] 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..bedb20aa7 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt @@ -0,0 +1,125 @@ +/* + * 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.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] +// 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) { + ContentGreen("Welcome to Nav3") { + Button(onClick = { + backStack.add(Product("123")) + }) { + Text("Click to navigate") + } + } + } + + is Product -> NavEntry(key) { + 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/decorators/DecoratorSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/decorators/DecoratorSnippets.kt new file mode 100644 index 000000000..4fec8c5d5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/decorators/DecoratorSnippets.kt @@ -0,0 +1,64 @@ +/* + * 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.decorators + +import android.util.Log +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation3.runtime.NavEntryDecorator +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import com.example.compose.snippets.navigation3.savingstate.Home +import kotlinx.serialization.Serializable + +// [START android_compose_navigation3_decorator_1] +// import androidx.navigation3.runtime.NavEntryDecorator +class CustomNavEntryDecorator : NavEntryDecorator( + decorate = { entry -> + Log.d("CustomNavEntryDecorator", "entry with ${entry.contentKey} entered composition and was decorated") + entry.Content() + }, + onPop = { contentKey -> Log.d("CustomNavEntryDecorator", "entry with $contentKey was popped") } +) +// [END android_compose_navigation3_decorator_1] + +@Serializable +data object Home : NavKey + +@Composable +fun DecoratorsBasic() { + // [START android_compose_navigation3_decorator_2] + + // import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator + NavDisplay( + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + remember { CustomNavEntryDecorator() } + ), + // [START_EXCLUDE] + backStack = rememberNavBackStack(Home), + entryProvider = entryProvider { + entry { Text("Welcome to Nav3") } + } + // [END_EXCLUDE] + ) + // [END android_compose_navigation3_decorator_2] +} 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..48179f68d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt @@ -0,0 +1,55 @@ +/* + * 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.runtime.Composable +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import kotlinx.serialization.Serializable + +// [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 + rememberSaveableStateHolderNavEntryDecorator(), + // 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..8de59b51f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt @@ -0,0 +1,202 @@ +/* + * 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.material.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +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.window.core.layout.WindowSizeClass +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND +import com.example.compose.snippets.touchinput.Button +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: Any, + val entry: NavEntry, + override val previousEntries: List>, +) : Scene { + override val entries: List> = listOf(entry) + override val content: @Composable () -> Unit = { entry.Content() } +} + +/** + * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the + * list. + */ +public class SinglePaneSceneStrategy : SceneStrategy { + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? = + SinglePaneScene( + key = entries.last().contentKey, + entry = entries.last(), + previousEntries = entries.dropLast(1) + ) +} +// [END android_compose_navigation3_scenes_2] + +// [START android_compose_navigation3_scenes_3] +// --- ListDetailScene --- +/** + * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. + * + */ +class ListDetailScene( + override val key: Any, + override val previousEntries: List>, + val listEntry: NavEntry, + val detailEntry: NavEntry, +) : Scene { + override val entries: List> = listOf(listEntry, detailEntry) + override val content: @Composable (() -> Unit) = { + Row(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.weight(0.4f)) { + listEntry.Content() + } + Column(modifier = Modifier.weight(0.6f)) { + detailEntry.Content() + } + } + } +} + +@Composable +fun rememberListDetailSceneStrategy(): ListDetailSceneStrategy { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + return remember(windowSizeClass) { + ListDetailSceneStrategy(windowSizeClass) + } +} + +// --- ListDetailSceneStrategy --- +/** + * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item + * is the backstack is a detail, and before it, at any point in the backstack is a list. + */ +class ListDetailSceneStrategy(val windowSizeClass: WindowSizeClass) : SceneStrategy { + + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { + + if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { + return null + } + + val detailEntry = + entries.lastOrNull()?.takeIf { it.metadata.containsKey(DETAIL_KEY) } ?: return null + val listEntry = entries.findLast { it.metadata.containsKey(LIST_KEY) } ?: return null + + // We use the list's contentKey to uniquely identify the scene. + // This allows the detail panes to be displayed instantly through recomposition, rather than + // having NavDisplay animate the whole scene out when the selected detail item changes. + val sceneKey = listEntry.contentKey + + return ListDetailScene( + key = sceneKey, + previousEntries = entries.dropLast(1), + listEntry = listEntry, + detailEntry = detailEntry + ) + } + + companion object { + internal const val LIST_KEY = "ListDetailScene-List" + internal const val DETAIL_KEY = "ListDetailScene-Detail" + + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed + * as a list in the [ListDetailScene]. + */ + fun listPane() = mapOf(LIST_KEY to true) + + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed + * as a list in the [ListDetailScene]. + */ + fun detailPane() = mapOf(DETAIL_KEY to true) + } +} +// [END android_compose_navigation3_scenes_3] + +// [START android_compose_navigation3_scenes_4] +// Define your navigation keys +@Serializable +data object ConversationList : NavKey + +@Serializable +data class ConversationDetail(val id: String) : NavKey + +@Composable +fun MyAppContent() { + val backStack = rememberNavBackStack(ConversationList) + val listDetailStrategy = rememberListDetailSceneStrategy() + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategy = listDetailStrategy, + entryProvider = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane() + ) { + Column(modifier = Modifier.fillMaxSize()) { + Text(text = "I'm a Conversation List") + Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) { + Text(text = "Open detail") + } + } + } + entry( + metadata = ListDetailSceneStrategy.detailPane() + ) { + Text(text = "I'm a Conversation Detail") + } + } + ) +} + +private fun NavBackStack.addDetail(detailRoute: ConversationDetail) { + + // Remove any existing detail routes, then add the new detail route + removeIf { it is ConversationDetail } + add(detailRoute) +} +// [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 new file mode 100644 index 000000000..e47a4ab2c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/material/MaterialScenesSnippets.kt @@ -0,0 +1,110 @@ +/* + * 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.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 = { 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] 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..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 @@ -31,3 +31,12 @@ 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..c47fdfda6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,8 +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-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" @@ -31,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 @@ -51,12 +53,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.3.0-alpha01" media3 = "1.8.0" # @keep minSdk = "35" @@ -77,6 +81,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 +105,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 +139,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 +194,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" }