diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index 8b644b694..47f65ba1f 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..4def0c40b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt @@ -0,0 +1,130 @@ +/* + * 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] 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..38d76b123 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/basic/BasicSnippets.kt @@ -0,0 +1,126 @@ +/* + * 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.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/savingstate/SavingStateSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt new file mode 100644 index 000000000..733191e39 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/savingstate/SavingStateSnippets.kt @@ -0,0 +1,57 @@ +/* + * 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.rememberSavedStateNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator +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 + 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..9ec3508c8 --- /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] 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..d71ab3f6b --- /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] 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 733b3cd7e..1575c7d11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,9 @@ androidx-fragment-ktx = "1.8.6" androidx-glance-appwidget = "1.1.1" androidx-lifecycle-compose = "2.8.7" androidx-lifecycle-runtime-compose = "2.8.7" +androidx-lifecycle-viewmodel-navigation3 = "1.0.0-SNAPSHOT" androidx-navigation = "2.8.9" +androidx-navigation3 = "1.0.0-SNAPSHOT" androidx-paging = "3.3.6" androidx-startup-runtime = "1.2.0" androidx-test = "1.6.1" @@ -48,12 +50,13 @@ junit = "4.13.2" kotlin = "2.1.20" kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" -kotlinxSerializationJson = "1.8.1" +kotlinxSerializationCore = "1.8.1" ksp = "2.1.20-2.0.0" maps-compose = "6.6.0" material = "1.13.0-alpha13" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" +material3-adaptive-navigation3 = "1.0.0-SNAPSHOT" media3 = "1.6.1" # @keep minSdk = "21" @@ -69,6 +72,7 @@ wearComposeFoundation = "1.4.1" wearComposeMaterial = "1.4.1" wearToolingPreview = "1.0.0" webkit = "1.13.0" +material3WindowSizeClassAndroid = "1.3.2" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } @@ -92,6 +96,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" } @@ -121,10 +127,13 @@ androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", 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" } @@ -170,10 +179,12 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.re 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" } + [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d2212b63..91e7a8d73 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -val snapshotVersion : String? = System.getenv("COMPOSE_SNAPSHOT_ID") +val snapshotVersion : String? = "13508953" pluginManagement { repositories {