UI Navigasi dengan Resep Scene Decorators
Resep ini menunjukkan cara menambahkan elemen UI seperti panel aplikasi atas dan panel navigasi atau kolom samping yang ingin Anda tambahkan di adegan, bukan di tingkat entri nav. Untuk melakukannya, API ini menggunakan API dekorator adegan.
Cara kerjanya
Class NavigationScene
Class NavigationScene adalah inti dari resep ini. Fungsi ini menerima Scene, class ukuran jendela saat ini, SharedTransitionScope, dan composable untuk kolom samping navigasi dan menu navigasi. Jika class ukuran lebar jendela adalah sedang atau lebih besar, rel navigasi akan dirender di tepi awal layar dengan konten di tepi akhir. Jika tidak, menu navigasi akan dirender di tepi bawah layar dengan konten di atasnya.
Merender elemen UI bersama hanya sekali
Selama transisi antar-adegan, kedua adegan disusun dan dirender secara bersamaan. Untuk elemen yang digunakan bersama di antara adegan, seperti panel navigasi atau rel, mungkin tidak diinginkan untuk menyusunnya di kedua adegan.
Misalnya, composable kolom navigasi dan panel di resep ini memiliki beberapa status internal yang tidak dapat diangkat (seperti status untuk animasi setelah item dipilih). Oleh karena itu, sebaiknya panggil composable tertentu hanya dari satu adegan dalam satu waktu.
Untuk mencapai perilaku yang diinginkan, resep ini menggabungkan API konten yang dapat dipindahkan dan elemen bersama Compose:
- Dengan menggunakan
movableContentOf, composable dapat mempertahankan statusnya saat dipindahkan di antara berbagai cabang komposisi yang sesuai dengan setiap adegan. - Dengan menggunakan API elemen bersama, aplikasi dapat mempertahankan posisi panel/rel navigasi sambil menganimasikan konten adegan yang telah dihias. Hal ini dilakukan menggunakan pengubah
sharedElementserta pengubah kustom,cacheSize, yang mempertahankan placeholder berukuran tepat dalam adegan yang tidak memanggil composable konten yang dapat dipindahkan.
Class NavigationSceneDecoratorStrategy
Class NavigationSceneDecoratorStrategy bertanggung jawab untuk membungkus adegan input dalam NavigationScene. Fungsi rememberNavigationSceneDecoratorStrategy menyederhanakan proses pembuatan NavigationSceneDecoratorStrategy dengan menangani pembuatan composable movableContentOf. Umumnya, NavigationSceneDecoratorStrategy harus menjadi salah satu, jika bukan item terakhir, dalam parameter sceneDecoratorStrategies sehingga dapat berisi semua konten aplikasi lainnya.
/* * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.navscenedecorator import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.remember import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.scenes.listdetail.rememberListDetailSceneStrategy import com.example.nav3recipes.ui.setEdgeToEdgeConfig class ResponsiveNavigationSceneDecoratorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setEdgeToEdgeConfig() setContent { SharedTransitionLayout { val navigationState = rememberNavigationState( startRoute = RouteA, topLevelRoutes = setOf(RouteA, RouteB, RouteC) ) val navigator = remember(navigationState) { Navigator(navigationState) } val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() val responsiveNavigationSceneDecoratorStrategy = rememberResponsiveNavigationSceneDecoratorStrategy<NavKey>( navBar = { NavBar(NAV_ITEMS, navigator) }, navRail = { NavRail(NAV_ITEMS, navigator) }, sharedTransitionScope = this ) val entryProvider = entryProvider { featureASection { id -> navigator.navigate(RouteA1(id)) } featureBSection { navigator.navigate(RouteB1) } featureCSection { navigator.navigate(RouteC1) } } NavDisplay( entries = navigationState.toEntries(entryProvider), sceneDecoratorStrategies = listOf(responsiveNavigationSceneDecoratorStrategy), sceneStrategies = listOf(listDetailStrategy), sharedTransitionScope = this, onBack = navigator::goBack ) } } } }
/* * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.navscenedecorator import androidx.compose.animation.EnterExitState import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.navigation3.scene.Scene import androidx.navigation3.scene.SceneDecoratorStrategy import androidx.navigation3.scene.SceneDecoratorStrategyScope import androidx.navigation3.ui.LocalNavAnimatedContentScope import androidx.window.core.layout.WindowSizeClass data class ResponsiveNavigationScene<T : Any>( private val scene: Scene<T>, private val sharedTransitionScope: SharedTransitionScope, private val windowSizeClass: WindowSizeClass, private val navBarContent: @Composable (() -> Unit), private val navRailContent: @Composable (() -> Unit), ) : Scene<T> by scene { override val key = scene::class to scene.key override val content = @Composable { val animatedContentScope = LocalNavAnimatedContentScope.current val isMovableContentCaller = animatedContentScope.transition.targetState == EnterExitState.Visible with(sharedTransitionScope) { if (windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)) { Row(Modifier.fillMaxSize()) { Box( modifier = Modifier .cacheSize(!isMovableContentCaller) .sharedElement( rememberSharedContentState("nav-rail"), animatedContentScope ) ) { if (isMovableContentCaller) { navRailContent() } } Box(modifier = Modifier.weight(1f)) { scene.content() } } } else { Column(Modifier.fillMaxSize()) { Box(modifier = Modifier.weight(1f)) { scene.content() } Box( modifier = Modifier .cacheSize(!isMovableContentCaller) .sharedElement( rememberSharedContentState("nav-bar"), animatedContentScope ) ) { if (isMovableContentCaller) { navBarContent() } } } } } } } @Composable fun <T : Any> rememberResponsiveNavigationSceneDecoratorStrategy( navBar: @Composable () -> Unit, navRail: @Composable () -> Unit, sharedTransitionScope: SharedTransitionScope, windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass ): ResponsiveNavigationSceneDecoratorStrategy<T> { val currentNavBar by rememberUpdatedState(navBar) val currentNavRail by rememberUpdatedState(navRail) val movableNavBar = remember { movableContentOf { currentNavBar() } } val movableNavRail = remember { movableContentOf { currentNavRail() } } return remember(windowSizeClass, sharedTransitionScope) { ResponsiveNavigationSceneDecoratorStrategy( windowSizeClass, sharedTransitionScope, movableNavBar, movableNavRail ) } } class ResponsiveNavigationSceneDecoratorStrategy<T : Any>( private val windowSizeClass: WindowSizeClass, private val sharedTransitionScope: SharedTransitionScope, private val navBarContent: @Composable () -> Unit, private val navRailContent: @Composable () -> Unit, ) : SceneDecoratorStrategy<T> { override fun SceneDecoratorStrategyScope<T>.decorateScene(scene: Scene<T>): Scene<T> { return ResponsiveNavigationScene( scene, sharedTransitionScope, windowSizeClass, navBarContent, navRailContent ) } }
/* * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.navscenedecorator import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Camera import androidx.compose.material.icons.filled.Face import androidx.compose.material.icons.filled.Home import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import com.example.nav3recipes.content.ContentBase import com.example.nav3recipes.content.ContentGreen import com.example.nav3recipes.content.ContentMauve import com.example.nav3recipes.content.ContentOrange import com.example.nav3recipes.content.ContentPurple import com.example.nav3recipes.scenes.listdetail.ListDetailScene import com.example.nav3recipes.ui.theme.colors import kotlinx.serialization.Serializable @Serializable data object RouteA : NavKey @Serializable data class RouteA1(val id: Int) : NavKey @Serializable data object RouteB : NavKey @Serializable data object RouteB1 : NavKey @Serializable data object RouteC : NavKey @Serializable data object RouteC1 : NavKey const val ITEM_COUNT = 20 fun EntryProviderScope<NavKey>.featureASection( onSubRouteClick: (Int) -> Unit, ) { entry<RouteA>( metadata = ListDetailScene.listPane() ) { Surface(modifier = Modifier.fillMaxHeight()) { var contentPadding by remember { mutableStateOf(PaddingValues()) } LazyColumn( modifier = Modifier .fillMaxSize() .onWindowInsetsOverlapChanged(WindowInsets.safeDrawing) { contentPadding = it }, contentPadding = contentPadding ) { items(List(ITEM_COUNT) { it + 1 }) { id -> ListItem( headlineContent = { Text("Item $id") }, leadingContent = { Box( modifier = Modifier .size(24.dp) .clip(RoundedCornerShape(4.dp)) .background(colors[id % colors.size]) ) }, modifier = Modifier .fillMaxWidth() .clickable(onClick = dropUnlessResumed { onSubRouteClick(id) }), ) } } } } entry<RouteA1>( metadata = ListDetailScene.detailPane() ) { key -> ContentBase( "Item ${key.id}", modifier = Modifier.background(colors[key.id % colors.size]) ) { var count by rememberSaveable { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text("Value: $count") } } } } fun EntryProviderScope<NavKey>.featureBSection( onSubRouteClick: () -> Unit, ) { entry<RouteB> { ContentGreen("Route B") { Column( horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = dropUnlessResumed { onSubRouteClick() }) { Text("Go to B1") } } } } entry<RouteB1> { ContentPurple("Route B1") { var count by rememberSaveable { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text("Value: $count") } } } } fun EntryProviderScope<NavKey>.featureCSection( onSubRouteClick: () -> Unit, ) { entry<RouteC> { ContentMauve("Route C") { Column(horizontalAlignment = Alignment.CenterHorizontally) { Button(onClick = dropUnlessResumed { onSubRouteClick() }) { Text("Go to C1") } } } } entry<RouteC1> { ContentOrange("Route C1") { var count by rememberSaveable { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text("Value: $count") } } } } data class NavBarItem( val navKey: NavKey, val icon: ImageVector, val description: String ) val NAV_ITEMS = listOf( NavBarItem(RouteA, Icons.Default.Home, "Route A"), NavBarItem(RouteB, Icons.Default.Face, "Route B"), NavBarItem(RouteC, Icons.Default.Camera, "Route C"), ) @Composable fun NavBar(navBarItems: List<NavBarItem>, navigator: Navigator) { NavBar( navBarItems = navBarItems, topLevelRoute = navigator.state.topLevelRoute, onNavItemClick = { navigator.navigate(it) } ) } @Composable fun NavBar(navBarItems: List<NavBarItem>, topLevelRoute: NavKey, onNavItemClick: (NavKey) -> Unit) { NavigationBar(Modifier.consumeWindowInsets(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))) { navBarItems.forEach { item -> NavigationBarItem( selected = item.navKey == topLevelRoute, onClick = { onNavItemClick(item.navKey) }, icon = { Icon( imageVector = item.icon, contentDescription = item.description ) }, label = { Text(item.description) }) } } } @Composable fun NavRail(navRailItems: List<NavBarItem>, navigator: Navigator) { NavRail( navRailItems = navRailItems, topLevelRoute = navigator.state.topLevelRoute, onNavItemClick = { navigator.navigate(it) } ) } @Composable fun NavRail( navRailItems: List<NavBarItem>, topLevelRoute: NavKey, onNavItemClick: (NavKey) -> Unit ) { NavigationRail { navRailItems.forEach { item -> NavigationRailItem( selected = item.navKey == topLevelRoute, onClick = { onNavItemClick(item.navKey) }, icon = { Icon( imageVector = item.icon, contentDescription = item.description ) }, label = { Text(item.description) }) } } }
/* * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.navscenedecorator import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.node.GlobalPositionAwareModifierNode import androidx.compose.ui.node.LayoutModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.invalidateMeasurement import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection /** * A modifier that caches the measured size of the component and optionally reuses it. For example, * this can be used to maintain the size of an element that contains moveable content. * * @param useCachedSize If true, the modifier will use the previously measured and cached size * (if available) instead of the current size. If false, it measures normally and caches the new size. */ fun Modifier.cacheSize(useCachedSize: Boolean): Modifier = this.then(CacheSizeElement(useCachedSize)) private data class CacheSizeElement( val useCachedSize: Boolean ) : ModifierNodeElement<CacheSizeNode>() { override fun create() = CacheSizeNode(useCachedSize) override fun update(node: CacheSizeNode) { node.useCachedSize = useCachedSize } override fun InspectorInfo.inspectableProperties() { name = "cacheSize" properties["useCachedSize"] = useCachedSize } } private class CacheSizeNode( useCachedSize: Boolean ) : Modifier.Node(), LayoutModifierNode { var useCachedSize: Boolean = useCachedSize set(value) { if (field != value) { field = value invalidateMeasurement() } } private var isSizeCached = false private var cachedSize: IntSize = IntSize.Zero override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val placeable = measurable.measure(constraints) val currentSize = IntSize(placeable.width, placeable.height) val size = if (useCachedSize && isSizeCached) { cachedSize } else { currentSize } cachedSize = size isSizeCached = true return layout(size.width, size.height) { placeable.placeRelative(0, 0) } } } /** * A modifier that calculates how much the component overlaps with the given [WindowInsets] * and reports the overlap as [PaddingValues]. * * This can be used to dynamically adjust the layout of content that is partially obscured * by system bars or other window insets, such as by using it as the `contentPadding` parameter * for a [androidx.compose.foundation.lazy.LazyColumn] or [androidx.compose.foundation.lazy.LazyRow] * * @param insets The [WindowInsets] to calculate the overlap against. * @param onOverlapChanged A callback invoked whenever the overlap changes, providing the overlap as [PaddingValues]. */ @Composable fun Modifier.onWindowInsetsOverlapChanged( insets: WindowInsets, onOverlapChanged: (PaddingValues) -> Unit ): Modifier { val density = LocalDensity.current val windowInfo = LocalWindowInfo.current val layoutDirection = LocalLayoutDirection.current return this.then( WindowInsetsOverlapElement( insets = insets, density = density, windowHeight = windowInfo.containerSize.height.toFloat(), windowWidth = windowInfo.containerSize.width.toFloat(), layoutDirection = layoutDirection, onOverlapChanged = onOverlapChanged ) ) } private data class WindowInsetsOverlapElement( val insets: WindowInsets, val density: Density, val windowHeight: Float, val windowWidth: Float, val layoutDirection: LayoutDirection, val onOverlapChanged: (PaddingValues) -> Unit ) : ModifierNodeElement<WindowInsetsOverlapNode>() { override fun create() = WindowInsetsOverlapNode( insets, density, windowHeight, windowWidth, layoutDirection, onOverlapChanged ) override fun update(node: WindowInsetsOverlapNode) { node.insets = insets node.density = density node.windowHeight = windowHeight node.windowWidth = windowWidth node.layoutDirection = layoutDirection node.onOverlapChanged = onOverlapChanged // Recalculate padding when modifier properties (like insets) change, // even if the component hasn't moved or changed size (no layout pass). node.calculatePadding() } override fun InspectorInfo.inspectableProperties() { name = "onWindowInsetsOverlapChanged" properties["insets"] = insets } } private class WindowInsetsOverlapNode( var insets: WindowInsets, var density: Density, var windowHeight: Float, var windowWidth: Float, var layoutDirection: LayoutDirection, var onOverlapChanged: (PaddingValues) -> Unit ) : Modifier.Node(), GlobalPositionAwareModifierNode { // Cache the layout coordinates so padding can be recalculated // when insets change without triggering a new global positioning pass. private var lastCoordinates: LayoutCoordinates? = null override fun onGloballyPositioned(coordinates: LayoutCoordinates) { lastCoordinates = coordinates calculatePadding() } fun calculatePadding() { val coordinates = lastCoordinates ?: return val screenRect = coordinates.boundsInWindow() val topOverlap = (insets.getTop(density) - screenRect.top).coerceAtLeast(0f) val bottomOverlap = (screenRect.bottom - (windowHeight - insets.getBottom(density))).coerceAtLeast(0f) val leftOverlap = (insets.getLeft(density, layoutDirection) - screenRect.left).coerceAtLeast(0f) val rightOverlap = (screenRect.right - (windowWidth - insets.getRight(density, layoutDirection))).coerceAtLeast(0f) with(density) { onOverlapChanged( PaddingValues.Absolute( left = leftOverlap.toDp(), top = topOverlap.toDp(), right = rightOverlap.toDp(), bottom = bottomOverlap.toDp() ) ) } } }
/* * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.navscenedecorator import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.serialization.NavKeySerializer import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer import com.example.nav3recipes.conditional.rememberNavBackStack /** * Create a navigation state that persists config changes and process death. */ @Composable fun rememberNavigationState( startRoute: NavKey, topLevelRoutes: Set<NavKey> ): NavigationState { val topLevelRoute = rememberSerializable( startRoute, topLevelRoutes, serializer = MutableStateSerializer(NavKeySerializer()) ) { mutableStateOf(startRoute) } val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } return remember(startRoute, topLevelRoutes) { NavigationState( startRoute = startRoute, topLevelRoute = topLevelRoute, backStacks = backStacks ) } } /** * State holder for navigation state. * * @param startRoute - the start route. The user will exit the app through this route. * @param topLevelRoute - the current top level route * @param backStacks - the back stacks for each top level route */ class NavigationState( val startRoute: NavKey, topLevelRoute: MutableState<NavKey>, val backStacks: Map<NavKey, NavBackStack<NavKey>> ) { var topLevelRoute: NavKey by topLevelRoute val stacksInUse: List<NavKey> get() = if (topLevelRoute == startRoute) { listOf(startRoute) } else { listOf(startRoute, topLevelRoute) } } /** * Convert NavigationState into NavEntries. */ @Composable fun NavigationState.toEntries( entryProvider: (NavKey) -> NavEntry<NavKey> ): SnapshotStateList<NavEntry<NavKey>> { val decoratedEntries = backStacks.mapValues { (_, stack) -> val decorators = listOf( rememberSaveableStateHolderNavEntryDecorator<NavKey>(), ) rememberDecoratedNavEntries( backStack = stack, entryDecorators = decorators, entryProvider = entryProvider ) } return stacksInUse.flatMap { decoratedEntries[it] ?: emptyList() }.toMutableStateList() }
/* * 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.navscenedecorator import androidx.navigation3.runtime.NavKey class Navigator(val state: NavigationState) { fun navigate(route: NavKey) { if (route in state.backStacks.keys) { // This is a top level route, just switch to it state.topLevelRoute = route } else { if (route is RouteA1) { state.backStacks[state.topLevelRoute]?.removeAll { it is RouteA1 } } state.backStacks[state.topLevelRoute]?.add(route) } } fun goBack() { val currentStack = state.backStacks[state.topLevelRoute] ?: error("Stack for ${state.topLevelRoute} not found") val currentRoute = currentStack.last() // If we're at the base of the current route, go back to the start route stack. if (currentRoute == state.topLevelRoute) { state.topLevelRoute = state.startRoute } else { currentStack.removeLastOrNull() } } }