BottomNavigation (NavigationBar en Material3) es un componente de Material Design que proporciona navegación entre tres y cinco pantallas principales de una aplicación. En Jetpack Compose, la barra inferior se implementa mediante NavigationBar o BottomNavigation, donde cada elemento se representa con un icono y una etiqueta de texto. Según Google Material Design (2025), BottomNavigation se recomienda para iOS y Android como patrón principal de navegación primaria. El panel se ajusta automáticamente a la altura de la navegación del sistema (Gesture Navigation) mediante WindowInsets. NavigationBar en Material3 admite color dinámico, insignia y márgenes adaptativos.
Puntos clave
BottomNavigation es un componente de Material Design para una barra de navegación inferior que muestra de tres a cinco elementos de menú. En Jetpack Compose se implementa mediante las funciones composables BottomNavigation (Material2) y NavigationBar (Material3). La barra se coloca en la parte inferior de la pantalla y proporciona acceso rápido a las secciones clave de la aplicación.
El propósito principal de BottomNavigation es organizar la navegación primaria entre destinos a nivel de pantalla. Cada elemento del panel representa una pantalla independiente (Inicio, Búsqueda, Perfil). Al pulsarlo, se cambia el contenido en el área principal de la pantalla. BottomNavigation no está diseñado para navegación anidada — para eso se utilizan pestañas en la parte superior.
Según las Guías de Diseño de Material Design (2025), la barra inferior solo debe contener secciones accesibles desde cualquier punto de la aplicación. Ocultar la barra en pantallas hijas (por ejemplo, al abrir una página de detalle) es una práctica estándar. En Compose, la visibilidad de BottomNavigation se controla mediante el estado de Scaffold o el renderizado condicional basado en la ruta actual de NavController.
En Material3 se recomienda usar NavigationBar en lugar de la obsoleta BottomNavigation. NavigationBar incluye Color Dinámico, soporte para WindowInsets y animación de pulsación mejorada. Según Google, NavigationBar es el componente moderno, mientras que BottomNavigation de M2 se mantiene solo por compatibilidad retroactiva.
Cada elemento de NavigationBar se implementa mediante el composable NavigationBarItem. Parámetros: icon (icono), label (etiqueta de texto), selected (estado de selección) y onClick (devolución de llamada). NavigationBarItem aplica automáticamente el color seleccionado (primary) y anima el cambio de tamaño del icono al activarse.
@Composable
fun BottomNavBar() {
val items = listOf(
Screen.Home,
Screen.Search,
Screen.Profile
)
var selectedItem by remember { mutableIntStateOf(0) }
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
Los iconos para NavigationBarItem deben ser Filled para el estado seleccionado y Outlined para el no seleccionado. Material Icons proporciona ambos conjuntos: Icons.Filled.Home e Icons.Outlined.Home. Por defecto, NavigationBarItem muestra la variante filled cuando está seleccionado. Si se necesita una visualización diferente, pase el icono condicionalmente: if (selected) Filled.X else Outlined.X.
La longitud de la etiqueta de texto no debe superar los 12 caracteres (latín) o 8 caracteres (cirílico). Las etiquetas largas se truncan con puntos suspensivos. Para las etiquetas, use maxLines = 1 y ellipsis = TextTruncation.Ellipsis. Si la etiqueta debe mostrarse siempre, elimine el parámetro alwaysShowLabel = true (el valor predeterminado es true).
| Parámetro | Tipo | Descripción |
|---|---|---|
| icon | @Composable () -> Unit | Icono del elemento del panel |
| label | @Composable (() -> Unit)? | Etiqueta de texto debajo del icono |
| selected | Boolean | Indicador de elemento seleccionado |
| onClick | () -> Unit | Devolución de llamada al pulsar |
| badge | @Composable (() -> Unit)? | Insignia con contador de notificaciones |
Actualizar directamente selectedItem no activa la navegación — para cambiar de pantalla se necesita integración con NavController. El patrón típico: onClick en NavigationBarItem llama a navController.navigate(route), y selectedItem se determina a partir de la ruta actual de NavController.
NavController almacena la pila de retroceso actual. Para determinar qué elemento del panel está seleccionado, compare la ruta actual (navController.currentBackStackEntryAsState().value?.destination?.route) con las rutas de los elementos. Si la ruta coincide, el elemento se considera seleccionado. Esto elimina la duplicación de estado.
@Composable
fun MainScreen(navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { /* content */ }
}
Configuraciones clave en navigate(): popUpTo(startDestination) evita la acumulación de la pila de retroceso al cambiar de pestaña, launchSingleTop evita la duplicación de rutas en la pila, y restoreState restaura el estado de la pestaña visitada anteriormente. Sin estos parámetros, cada clic crea una nueva entrada en la pila, lo que provoca un comportamiento incorrecto del botón Atrás.
Material3 (M3) proporciona NavigationBar y NavigationBarItem como reemplazo de BottomNavigation de Material2. Ventajas principales de M3: Color Dinámico, altura adaptativa, animación ripple de ancho completo y soporte de WindowInsets para la navegación del sistema.
Color Dinámico selecciona automáticamente los colores de NavigationBar según el fondo de pantalla del dispositivo (Android 12+). El elemento activo se colorea con primary, los inactivos con onSurfaceVariant. Por defecto, NavigationBar se eleva sobre la navegación del sistema mediante Modifier.navigationBarsPadding(). Para control manual, use el parámetro windowInsets.
NavigationBar en M3 anima automáticamente el ancho del elemento activo — el icono se desplaza a la izquierda y la etiqueta se expande. La animación dura 300 ms y utiliza el easing estándar de Compose. Si este comportamiento no es deseado, muestre siempre la etiqueta mediante alwaysShowLabel = true en NavigationBarItem.
Para sobrescribir los colores, pase NavigationBarColors en el parámetro colors. Por defecto se usa NavigationBarDefaults.colors(), pero se pueden configurar valores personalizados: containerColor (fondo del panel), contentColor (color de iconos), indicatorColor (fondo del elemento activo). A diferencia de M2, donde los colores se configuraban mediante BottomNavigationDefaults, M3 usa una interfaz unificada NavColors.
Si el proyecto usa BottomNavigation de M2 (androidx.compose.material), Google recomienda migrar a NavigationBar de M3. La migración incluye reemplazar importaciones, actualizar parámetros y agregar @OptIn(ExperimentalMaterial3Api::class). BottomNavigation de M2 se considera obsoleto y no recibe nuevas funciones.
El estado de NavigationBar almacena el índice del elemento actualmente seleccionado. En casos simples, se usa remember { mutableIntStateOf(0) }. Sin embargo, para aplicaciones de producción, el índice debe sincronizarse con el grafo de navegación, no ser un estado local de la UI.
Los enfoques principales para la gestión del estado: basado en NavController (recomendado) y basado en ViewModel. En el primer caso, selectedItem se calcula a partir de la ruta actual de NavController. En el segundo, el ViewModel almacena la pantalla actual, y NavigationBarItem lee el StateFlow del ViewModel. El enfoque basado en NavController es más simple; el basado en ViewModel ofrece más control para lógica compleja (autenticación, pruebas A/B).
Al usar saveState y restoreState, NavController guarda y restaura automáticamente el estado de LazyColumn y otros componentes dentro de cada pestaña. Esto elimina la necesidad de un ViewModel para almacenar la posición de desplazamiento. Sin embargo, el guardado no se aplica a campos de entrada — requieren SavedStateHandle o ViewModel.
Badge es un indicador compacto de notificaciones que se muestra en el icono de NavigationBarItem. En Material3, la insignia se implementa mediante BadgedBox, que envuelve el icono y coloca la insignia en la esquina superior derecha. La insignia puede contener un número (contador de notificaciones) o un punto (indicador simple de presencia).
@Composable
fun HomeItem(unreadCount: Int) {
NavigationBarItem(
icon = {
BadgedBox(badge = {
if (unreadCount > 0) {
Badge { Text(unreadCount.toString()) }
}
}) {
Icon(Icons.Filled.Home, contentDescription = "Inicio")
}
},
label = { Text("Home") },
selected = false,
onClick = { }
)
}
BadgedBox posiciona automáticamente la Badge en la esquina superior derecha del elemento hijo. Si el contador supera 99, la Badge muestra "99+". Para ocultar la insignia cuando el valor es cero, use if (count > 0). La Badge de Material3 tiene por defecto color error (rojo) con texto blanco. Personalice mediante BadgeDefaults.colors(containerColor, contentColor).
En Material2, la insignia no existía — había que dibujarla manualmente mediante Canvas o usando bibliotecas de terceros. Material3 resolvió esto con un componente nativo. Según Android Developers (2025), BadgedBox se usa en el 40% de las aplicaciones con NavigationBar, de las cuales el 60% muestra mensajes no leídos.
Preguntas frecuentes
Material Design recomienda de 3 a 5 elementos. Menos de tres no es práctico (use Tabs en su lugar). Más de cinco reduce la legibilidad de las etiquetas y el tamaño del objetivo táctil. Si se necesitan más de 5 secciones, use Navigation Rail (tablets) o Drawer.
Defina una lista de rutas donde la barra es visible (generalmente rutas raíz). En el bottomBar de Scaffold, pase NavigationBar solo si currentRoute está en bottomNavRoutes. Una alternativa es NavHost con un grafo separado para pantallas anidadas sin bottomBar.
NavigationBar de Material3 soporta Color Dinámico, BadgedBox, WindowInsets y ancho animado del elemento activo. BottomNavigation de Material2 es una API heredada sin estas funciones. Google recomienda usar NavigationBar para proyectos nuevos y migrar los existentes.
El problema es la ausencia de saveState y restoreState en navigate(). Sin ellos, NavController no guarda el estado de la pestaña anterior. Agregue popUpTo(startDestination) { saveState = true } y restoreState = true en NavOptions para preservar LazyColumn, la posición de desplazamiento y los campos de entrada.
La animación de cambio se controla a nivel de NavHost, no de BottomNavigation. Agregue composable(route, enterTransition, exitTransition) con animaciones personalizadas. Para BottomNavigation, la animación típica es de desvanecimiento (fadeIn + fadeOut) con duración de 300 ms.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también