Receta de IU común
En esta receta, se muestra cómo implementar un patrón de IU de navegación común con una barra de navegación inferior y varias pilas de actividades, en el que cada pestaña de la barra de navegación tiene su propio historial de navegación.
Cómo funciona
En este ejemplo, hay tres destinos de nivel superior: Home, ChatList y Camera. El destino ChatList también tiene una subruta, ChatDetail.
TopLevelBackStack
El núcleo de esta receta es la clase TopLevelBackStack, que es responsable de administrar el estado de navegación. Funciona de la siguiente manera:
- Mantiene una pila de actividades separada para cada destino de nivel superior (pestaña).
- Realiza un seguimiento del destino de nivel superior seleccionado actualmente.
- Proporciona una sola pila de actividades aplanada que puede usar el elemento
NavDisplaycomponible. Esta pila de actividades aplanada es una combinación de las pilas de actividades individuales de todas las pestañas.
Estructura de la IU
La IU se compila con un elemento Scaffold componible, con un NavigationBar como bottomBar.
- El
NavigationBarmuestra un elemento para cada destino de nivel superior. Cuando se hace clic en un elemento, se llama atopLevelBackStack.addTopLevelpara cambiar a la pestaña correspondiente y conservar el historial de navegación de cada pestaña. - La función de componibilidad
NavDisplayse coloca en el área de contenido deScaffold. Es responsable de mostrar la pantalla actual según la pila de actividades aplanada que proporcionaTopLevelBackStack.
Este enfoque permite un patrón de navegación común en el que los usuarios pueden cambiar entre diferentes secciones de la app, y cada sección mantiene su propio historial de navegación.
Preservación del estado
Es importante tener en cuenta cómo se administra el estado de navegación en esta receta. Cuando un usuario abandona un destino de nivel superior (p.ej., presionando el botón Atrás hasta que regresa a una pestaña anterior), se borra todo el historial de navegación de ese destino. No se guarda el estado. Cuando el usuario vuelva a esa pestaña más tarde, comenzará desde su pantalla inicial.
Nota: En este ejemplo, la ruta Home puede moverse por encima de las rutas ChatList y Camera, lo que significa que navegar hacia atrás desde Home no necesariamente hace que se salga de la app. La app se cerrará cuando el usuario vuelva desde una sola ruta de nivel superior restante en la pila de actividades.
/* * 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 * * 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.commonui import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Face import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.content.ContentBlue import com.example.nav3recipes.content.ContentGreen import com.example.nav3recipes.content.ContentPurple import com.example.nav3recipes.content.ContentRed import com.example.nav3recipes.ui.setEdgeToEdgeConfig private sealed interface TopLevelRoute { val icon: ImageVector } private data object Home : TopLevelRoute { override val icon = Icons.Default.Home } private data object ChatList : TopLevelRoute { override val icon = Icons.Default.Face } private data object ChatDetail private data object Camera : TopLevelRoute { override val icon = Icons.Default.PlayArrow } private val TOP_LEVEL_ROUTES : List<TopLevelRoute> = listOf(Home, ChatList, Camera) class CommonUiActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { val topLevelBackStack = remember { TopLevelBackStack<Any>(Home) } Scaffold( bottomBar = { NavigationBar { TOP_LEVEL_ROUTES.forEach { topLevelRoute -> val isSelected = topLevelRoute == topLevelBackStack.topLevelKey NavigationBarItem( selected = isSelected, onClick = { topLevelBackStack.addTopLevel(topLevelRoute) }, icon = { Icon( imageVector = topLevelRoute.icon, contentDescription = null ) } ) } } } ) { _ -> NavDisplay( backStack = topLevelBackStack.backStack, onBack = { topLevelBackStack.removeLast() }, entryProvider = entryProvider { entry<Home>{ ContentRed("Home screen") } entry<ChatList>{ ContentGreen("Chat list screen"){ Button(onClick = dropUnlessResumed { topLevelBackStack.add(ChatDetail) }) { Text("Go to conversation") } } } entry<ChatDetail>{ ContentBlue("Chat detail screen") } entry<Camera>{ ContentPurple("Camera screen") } }, ) } } } } class TopLevelBackStack<T: Any>(startKey: T) { // Maintain a stack for each top level route private var topLevelStacks : LinkedHashMap<T, SnapshotStateList<T>> = linkedMapOf( startKey to mutableStateListOf(startKey) ) // Expose the current top level route for consumers var topLevelKey by mutableStateOf(startKey) private set // Expose the back stack so it can be rendered by the NavDisplay val backStack = mutableStateListOf(startKey) private fun updateBackStack() = backStack.apply { clear() addAll(topLevelStacks.flatMap { it.value }) } fun addTopLevel(key: T){ // If the top level doesn't exist, add it if (topLevelStacks[key] == null){ topLevelStacks.put(key, mutableStateListOf(key)) } else { // Otherwise just move it to the end of the stacks topLevelStacks.apply { remove(key)?.let { put(key, it) } } } topLevelKey = key updateBackStack() } fun add(key: T){ topLevelStacks[topLevelKey]?.add(key) updateBackStack() } fun removeLast(){ val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() // If the removed key was a top level key, remove the associated top level stack topLevelStacks.remove(removedKey) topLevelKey = topLevelStacks.keys.last() updateBackStack() } }