BottomNavigation: qué es, estructura y configuración en Jetpack Compose

Autor: IT Sectr Publicado: 2026-06-29 Tiempo de lectura: 8 min

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 — barra de navegación inferior para 3–5 pantallas, implementada mediante NavigationBar en Material3
  • NavigationBarItem — elemento del panel que contiene icono, etiqueta e insignia para notificaciones
  • Conexión con NavHost — al seleccionar un elemento se ejecuta navigate() en NavController para cambiar el grafo
  • Material3 añade Color Dinámico y manejo automático de la navegación del sistema mediante WindowInsets
  • Badge — insignia de notificaciones en el icono, implementada mediante BadgedBox con un contador

¿Qué es BottomNavigation en Jetpack Compose?

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.

kotlin
@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ámetroTipoDescripción
icon@Composable () -> UnitIcono del elemento del panel
label@Composable (() -> Unit)?Etiqueta de texto debajo del icono
selectedBooleanIndicador de elemento seleccionado
onClick() -> UnitDevolución de llamada al pulsar
badge@Composable (() -> Unit)?Insignia con contador de notificaciones

Conexión de BottomNavigation con NavHost

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.

kotlin
@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.

NavigationBar en Material3: Color Dinámico y adaptación

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.

Personalización de colores de NavigationBar

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.

Insignias de notificaciones y contadores en elementos

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).

kotlin
@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

¿Cuántos elementos debe tener BottomNavigation?

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.

¿Cómo ocultar BottomNavigation en pantallas hijas?

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.

¿Cuál es la diferencia entre NavigationBar (M3) y BottomNavigation (M2)?

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.

¿Por qué se recrea la pantalla al hacer clic en un elemento de BottomNavigation?

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.

¿Cómo personalizar la animación de cambio entre pestañas?

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

  • BottomNavigation (NavigationBar en M3) — barra inferior para cambiar entre 3–5 pantallas principales de la aplicación
  • NavigationBarItem — elemento del panel con icono, etiqueta, insignia y estado de selección
  • Integración con NavController requiere popUpTo, launchSingleTop y restoreState para una gestión correcta de la pila
  • NavigationBar de Material3 soporta Color Dinámico, BadgedBox, WindowInsets y animación del elemento activo
  • BadgedBox — componente para insignias de notificaciones con número o punto en el icono del elemento
  • Estado de selección se calcula desde currentBackStackEntryAsState para sincronización con NavController
  • Para ocultar la barra en pantallas hijas, use renderizado condicional en el bottomBar de Scaffold basado en una lista de rutas

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.

Discutir el proyecto

Lea también