diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index e21218d6c..356477115 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -96,6 +96,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) @@ -138,7 +139,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..32241a064 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/animations/AnimationSnippets.kt @@ -0,0 +1,151 @@ +/* + * 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.SharedTransitionLayout +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.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.metadata +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 = metadata { + put(NavDisplay.TransitionKey) { + // Slide new content up, keeping the old content in place underneath + slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(1000) + ) togetherWith ExitTransition.KeepUntilTransitionsFinished + } + put(NavDisplay.PopTransitionKey) { + // Slide old content down, revealing the new content in place underneath + EnterTransition.None togetherWith + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(1000) + ) + } + put(NavDisplay.PredictivePopTransitionKey) { + // 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] + +@Composable +fun SharedTransitionScopeNavDisplay() { +// [START android_compose_navigation3_animations_2] + SharedTransitionLayout { + NavDisplay( + // [START_EXCLUDE] + backStack = emptyList(), + entryProvider = entryProvider { }, + // [END_EXCLUDE] + sharedTransitionScope = this + ) + } +// [END android_compose_navigation3_animations_2] +} \ 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..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/metadata/MetadataSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/metadata/MetadataSnippets.kt new file mode 100644 index 000000000..9d397f778 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/metadata/MetadataSnippets.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2026 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.metadata + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.NavMetadataKey +import androidx.navigation3.runtime.contains +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.get +import androidx.navigation3.runtime.metadata +import androidx.navigation3.scene.Scene +import androidx.navigation3.scene.SceneStrategy +import androidx.navigation3.scene.SceneStrategyScope + +data object Home : NavKey +data class Conversation(val id: String) : NavKey + +val condition = true + +fun manualEntryProvider(key: Any) { + // [START android_compose_navigation3_metadata_1] + when (key) { + is Home -> NavEntry(key, metadata = mapOf("key" to "value")) {} + } + // [END android_compose_navigation3_metadata_1] +} + +fun dslEntryProvider() { + entryProvider { + // [START android_compose_navigation3_metadata_2] + entry(metadata = mapOf("key" to "value")) { /* ... */ } + entry(metadata = { key: Conversation -> + mapOf("key" to "value: ${key.id})") + }) { /* ... */ } + // [END android_compose_navigation3_metadata_2] + } +} + +// [START android_compose_navigation3_metadata_3] +// For classes such as scene strategies or nav entry decorators, you can define the keys +// as nested object. +class MySceneStrategy : SceneStrategy { + + // [START_EXCLUDE] + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { + TODO("Not yet implemented") + } + // [END_EXCLUDE] + + object MyStringMetadataKey : NavMetadataKey +} + +// An example from NavDisplay. +// Because NavDisplay is a function, the metadata keys are defined in an object with the same name. +public object NavDisplay { + + public object TransitionKey : + NavMetadataKey>.() -> ContentTransform> +} +// [END android_compose_navigation3_metadata_3] + +val entryProvider = entryProvider { + // [START android_compose_navigation3_metadata_4] + entry( + metadata = metadata { + put(NavDisplay.TransitionKey) { fadeIn() togetherWith fadeOut() } + // An additional benefit of the metadata DSL is the ability to use conditional logic + if (condition) { + put(MySceneStrategy.MyStringMetadataKey, "Hello, world!") + } + } + ) { + // ... + } + // [END android_compose_navigation3_metadata_4] +} + +val metadata = emptyMap() + +// [START android_compose_navigation3_metadata_5] +// import androidx.navigation3.runtime.contains +// import androidx.navigation3.runtime.get + +val hasMyString: Boolean = metadata.contains(MySceneStrategy.MyStringMetadataKey) +val myString: String? = metadata[MySceneStrategy.MyStringMetadataKey] +// [END android_compose_navigation3_metadata_5] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/modularization/ModularizationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/modularization/ModularizationSnippets.kt new file mode 100644 index 000000000..b666fa050 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/modularization/ModularizationSnippets.kt @@ -0,0 +1,127 @@ +/* + * 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.modularization + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.compose.snippets.navigation3.ContentGreen +import com.example.compose.snippets.navigation3.ContentRed +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet +import javax.inject.Inject +import kotlin.collections.emptyList + +data object KeyA : NavKey +data object KeyA2 : NavKey + +// [START android_compose_navigation3_modularization_1] +// import androidx.navigation3.runtime.EntryProviderScope +// import androidx.navigation3.runtime.NavKey + +fun EntryProviderScope.featureAEntryBuilder() { + entry { + ContentRed("Screen A") { + // Content for screen A + } + } + entry { + ContentGreen("Screen A2") { + // Content for screen A2 + } + } +} +// [END android_compose_navigation3_modularization_1] + + +@Composable +fun ModularizationApp() { + // [START android_compose_navigation3_modularization_2] + // import androidx.navigation3.runtime.entryProvider + // import androidx.navigation3.ui.NavDisplay + NavDisplay( + entryProvider = entryProvider { + featureAEntryBuilder() + }, + // [START_EXCLUDE] + backStack = emptyList() + // [END_EXCLUDE] + ) + // [END android_compose_navigation3_modularization_2] + +} + +// [START android_compose_navigation3_modularization_3] +// import dagger.Module +// import dagger.Provides +// import dagger.hilt.InstallIn +// import dagger.hilt.android.components.ActivityRetainedComponent +// import dagger.multibindings.IntoSet + +@Module +@InstallIn(ActivityRetainedComponent::class) +object FeatureAModule { + + @IntoSet + @Provides + fun provideFeatureAEntryBuilder() : EntryProviderScope.() -> Unit = { + featureAEntryBuilder() + } +} +// [END android_compose_navigation3_modularization_3] + +// [START android_compose_navigation3_modularization_4] +// import android.os.Bundle +// import androidx.activity.ComponentActivity +// import androidx.activity.compose.setContent +// import androidx.navigation3.runtime.EntryProviderScope +// import androidx.navigation3.runtime.NavKey +// import androidx.navigation3.runtime.entryProvider +// import androidx.navigation3.ui.NavDisplay +// import javax.inject.Inject + +class MainActivity : ComponentActivity() { + + @Inject + lateinit var entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope.() -> Unit> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + NavDisplay( + entryProvider = entryProvider { + entryBuilders.forEach { builder -> this.builder() } + }, + // [START_EXCLUDE] + backStack = emptyList() + // [END_EXCLUDE] + ) + } + } +} +// [END android_compose_navigation3_modularization_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..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/scenedecorators/SceneDecoratorSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenedecorators/SceneDecoratorSnippets.kt new file mode 100644 index 000000000..198cd4a77 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenedecorators/SceneDecoratorSnippets.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2026 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.scenedecorators + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.scene.Scene +import androidx.navigation3.scene.SceneDecoratorStrategy +import androidx.navigation3.scene.SceneDecoratorStrategyScope +import androidx.navigation3.ui.NavDisplay + +// [START android_compose_navigation3_scenedecorators_1] +class MySceneDecoratorStrategy : SceneDecoratorStrategy { + + // [START_EXCLUDE silent] + private fun shouldDecorate(scene: Scene): Boolean { + return true + } + // [END_EXCLUDE] + + override fun SceneDecoratorStrategyScope.decorateScene(scene: Scene): Scene { + // `shouldDecorate` determines if the scene should be decorated based on scene.metadata, + // scene.entries.metadata, or any other relevant state. + return if (shouldDecorate(scene)) { + MyDecoratingScene(scene) + } else { + scene + } + } + +} + +class MyDecoratingScene(scene: Scene) : Scene { + + // [START_EXCLUDE] + override val key = scene.key + override val entries = scene.entries + override val previousEntries = scene.previousEntries + // [END_EXCLUDE] + + override val content = @Composable { + scene.content() + } +} +// [END android_compose_navigation3_scenedecorators_1] + +@Composable +fun SceneDecoratorNavDisplay() { + val firstSceneDecoratorStrategy = remember { MySceneDecoratorStrategy() } + val secondSceneDecoratorStrategy = remember { MySceneDecoratorStrategy() } + + // [START android_compose_navigation3_scenedecorators_2] + NavDisplay( + // [START_EXCLUDE] + backStack = emptyList(), + entryProvider = entryProvider { }, + // [END_EXCLUDE] + sceneDecoratorStrategies = listOf(firstSceneDecoratorStrategy, secondSceneDecoratorStrategy) + ) + // [END android_compose_navigation3_scenedecorators_2] +} + +// [START android_compose_navigation3_scenedecorators_3] +class CopyingScene(scene: Scene) : Scene { + override val entries = scene.entries + override val previousEntries = scene.previousEntries + override val metadata = scene.metadata + + // [START_EXCLUDE] + override val key = scene.key + override val content = @Composable { + scene.content() + } + // [END_EXCLUDE] +} +// [END android_compose_navigation3_scenedecorators_3] + +// [START android_compose_navigation3_scenedecorators_4] +class DerivedKeyScene(scene: Scene) : Scene { + override val key = scene::class to scene.key + + // [START_EXCLUDE] + override val entries = scene.entries + override val previousEntries = scene.previousEntries + override val content = @Composable { + scene.content() + } + // [END_EXCLUDE] +} +// [END android_compose_navigation3_scenedecorators_4] \ No newline at end of file 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..5354d07c0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/scenes/ScenesSnippets.kt @@ -0,0 +1,209 @@ +/* + * 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.NavMetadataKey +import androidx.navigation3.runtime.contains +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.metadata +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.contains(DetailKey) } ?: return null + val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: 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 + ) + } + + object ListKey : NavMetadataKey + object DetailKey : NavMetadataKey + companion object { + + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed + * as a list in the [ListDetailScene]. + */ + fun listPane() = metadata { + put(ListKey, true) + } + + /** + * Helper function to add metadata to a [NavEntry] indicating it can be displayed + * as a list in the [ListDetailScene]. + */ + fun detailPane() = metadata { + put(DetailKey, 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() }, + sceneStrategies = listOf(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..d0792831f --- /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() }, + sceneStrategies = listOf(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 ea2869e83..f01ca8c58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ androidx-fragment = "1.8.9" androidx-glance-appwidget = "1.1.1" androidx-lifecycle-compose = "2.10.0" androidx-lifecycle-runtime-compose = "2.10.0" +androidx-lifecycle-viewmodel-navigation3 = "2.10.0-beta01" androidx-navigation = "2.9.7" androidx-navigation3 = "1.1.0-beta01" androidx-paging = "3.4.2" @@ -63,7 +64,7 @@ junit = "4.13.2" kotlin = "2.3.10" kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" -kotlinxSerializationJson = "1.10.0" +kotlinxSerialization = "1.10.0" ksp = "2.3.6" ktlint = "1.5.0" lifecycleService = "2.10.0" @@ -71,6 +72,7 @@ maps-compose = "8.2.1" material = "1.14.0-alpha10" material3-adaptive = "1.2.0" material3-adaptive-navigation-suite = "1.4.0" +material3-adaptive-navigation3 = "1.3.0-alpha09" media3 = "1.9.2" media3Ui = "1.9.2" # @keep @@ -131,6 +133,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" } @@ -172,6 +176,7 @@ 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" } @@ -249,7 +254,8 @@ kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines- kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-metadata-jvm = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" } -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 = "kotlinxSerialization" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } reactive-streams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactive-streams" }