Recette d'UI de navigation avec des décorateurs de scène
Cette recette montre comment ajouter des éléments d'interface utilisateur tels que des barres d'application supérieures et des barres de navigation ou des rails que vous souhaitez ajouter au niveau de la scène plutôt qu'au niveau de l'entrée de navigation. Pour ce faire, il utilise l'API du décorateur de scène.
Fonctionnement
La classe NavigationScene
La classe NavigationScene est au cœur de cette recette. Il accepte un Scene, la classe de taille de fenêtre actuelle, un SharedTransitionScope et des composables pour une barre de navigation et un rail de navigation. Si la classe de taille de la largeur de la fenêtre est moyenne ou supérieure, le rail de navigation s'affiche sur le bord de début de l'écran et le contenu sur le bord de fin. Sinon, la barre de navigation s'affiche en bas de l'écran et le contenu en haut.
Afficher les éléments d'UI partagés une seule fois
Lors des transitions entre les scènes, les deux scènes sont composées et rendues en même temps. Pour les éléments partagés entre les scènes, tels qu'une barre de navigation ou un rail, il peut être préférable qu'ils ne soient pas composés dans les deux scènes.
Par exemple, les composables de barre de navigation et de rail de cette recette ont un état interne qui ne peut pas être hissé (comme l'état des animations après la sélection d'un élément). Il est donc préférable d'appeler le composable donné à partir d'une seule scène à la fois.
Pour obtenir le comportement souhaité, cette recette combine les API movable content et shared element de Compose :
- En utilisant
movableContentOf, il est possible de conserver l'état du composable lorsqu'il est déplacé entre les différentes branches de la composition correspondant à chaque scène. - En utilisant les API d'élément partagé, il est possible de maintenir la barre de navigation en place tout en animant le contenu des scènes décorées. Pour ce faire, nous utilisons le modificateur
sharedElementainsi qu'un modificateur personnalisé,cacheSize, qui conserve un espace réservé de la bonne taille dans la scène sans appeler le composable de contenu déplaçable.
La classe NavigationSceneDecoratorStrategy
La classe NavigationSceneDecoratorStrategy est chargée d'encapsuler la scène d'entrée dans un NavigationScene. La fonction rememberNavigationSceneDecoratorStrategy simplifie le processus de création d'un NavigationSceneDecoratorStrategy en gérant la création des composables movableContentOf. En général, NavigationSceneDecoratorStrategy doit être l'un des derniers éléments du paramètre sceneDecoratorStrategies, voire le dernier, afin qu'il puisse contenir tout le reste du contenu de l'application.
/* * 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() } } }