Introducción
¿Qué Es Una Navigation Bar?
La Navigation Bar es un componente de navegación que se ubica en la parte inferior de la pantalla de pantallas pequeñas (teléfonos) con el objetivo de proveer a los usuarios la navegación entre tres a cinco destinos principales.
Como ves, se compone de ítems con un icono representativo y una etiqueta corta asociada. La acción de toque de un ítem desencadena un cambio de contenido principal en la pantalla (dejando al icono activo).
¿Cuándo Usar Navigation Bar En Android?
Cuando necesites mostrar al usuario la posibilidad de visitar de 3 a 5 destinos principales en tu app de forma persistente. Es decir, los destinos deben estar disponibles desde cualquier parte de la app y no deben variar para mantener la consistencia.
Qué Aprenderás En Este Tutorial
En este tutorial aprenderás a crear una Navigation Bar en Android Studio usando Jetpack Compose. Comenzarás configurando un nuevo proyecto, diseñando la estructura básica de navegación y personalizando estilos, colores e iconos. Además, implementarás características dinámicas como badges de notificaciones y animaciones para el indicador de selección.
Crear Proyecto De Navigation Bar En Android Studio
Prerrequisitos
Para comprender fluidamente los ejemplos que construiremos en este tutorial necesitarás haber aprendido sobre Badges y Navigation Component.
Crear Proyecto
El primer paso es crear un nuevo proyecto en Android Studio llamado Navigation Bar. Usa la plantilla Empty Activity, especifica como nombre de la actividad principal NavigationBarActivity y confirma su construcción.
Definir Dependencias Gradle
Debido a que usaremos Jetpack Compose ya conoces qué elementos incluir. Adicionalmente estaremos usando Navigation Component, por lo que modifica build.gradle.kts
de la aplicación para incluir:
dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
}
Definir Pantalla Principal
NavigationBarScreen
A continuación, añade un nuevo archivo Kotlin llamado NavigationBarScreen.kt
y define una función componible con el mismo nombre. Dicha función debe contener inicialmente un Scaffold
con una TopAppBar
en su parámetro topBar
y la invocación de una función llamada ExampleNavigationBar
en bottomBar
:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavigationBarScreen() {
Scaffold(
topBar = { TopBar() },
bottomBar = { ExampleNavigationBar() }
) {
Text(
modifier = Modifier
.padding(it)
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 8.dp),
text = "Contenido principal de la pantalla"
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun TopBar() {
CenterAlignedTopAppBar(
title = {
Text(stringResource(R.string.app_name))
}
)
}
@Composable
private fun ExampleNavigationBar() {
// Aquí va nuestra NavigationBar
}
@Composable
@Preview
private fun NavigationBarScreenPreview() {
NavigationBarScreen()
}
Vincular Pantalla Con Actividad
Para este ejemplo NavigationBarScreen
será el componente de UI donde iniciará nuestra App, por lo que lo invocaremos en setContent()
al interior de onCreate()
en NavigationBarActivity
:
class NavigationBarActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
NavigationBarTheme {
NavigationBarScreen()
}
}
}
}
Código Completo En GitHub
Si acceder al código completo para analizar el panorama general de los ejemplos usados en este tutorial, visita el repositorio en GitHub:
Crear Navigation Bar Con Jetpack Compose
El siguiente paso es implementar un ejemplo básico de Navigation Bar con los destinos: Inicio, Explorar, Avisos y Perfil; tal como vez en la ilustración anterior.
Esto requiere de la invocación del componente NavigationBar
para representar la navegación inferior y del componente NavigationBarItem
para materializar a los ítems en su interior.
Crear Data Class Para Ítems
Expresemos el estado de los ítems como una data class que agrupe las características de las que hemos hablado anteriormente:
- Icono en selección
- Icono sin selección
- Etiqueta
Con esto en mente, añade una nueva clase llamada NavigationBarItem
con la siguiente definición:
data class NavigationBarItem(
val label: String,
val icon: ImageVector,
val selectedIcon: ImageVector
)
Ahora bien, podríamos pasar una lista List<NavigationBarItem>
como parámetro a la función NavigationBarExample()
para que procese tal cual los elementos, pero como tenemos una invariante de cantidad de ítems, añadiremos otra clase para forzar esa regla:
data class NavigationBarItems(
val items: List<NavigationBarItem>,
) {
init {
require(items.size >= 3) {
"La Navigation Bar debe tener al menos 3 items. " +
"Usa Tabs en su lugar si tienes menos."
}
require(items.size <= 5) {
"La Navigation Bar debe tener como máximo 5 items. " +
"Usa Navigation Drawer en su lugar si tienes más."
}
}
}
Tal como se ve, NavigationBarItems
recibe la lista de los destinos y verifica en init()
que se cumplan las condiciones de tener entre 3 a 5 elementos. De lo contrario se lanzarán excepciones para evitar una creación que no cumpla las reglas.
Definir Items
Debido a que ya sabemos que tendremos cuatro ítems en la navigation bar, entonces declararemos una instancia de NavigationBarItems
como variable de alto nivel en un nuevo archivo NavigationBarItemsExample.kt
.
val ItemsExample = NavigationBarItems(
items = listOf(
NavigationBarItem(
label = "Inicio",
icon = Icons.Outlined.Home,
selectedIcon = Icons.Filled.Home
),
NavigationBarItem(
label = "Explorar",
icon = Icons.Outlined.Search,
selectedIcon = Icons.Filled.Search
),
NavigationBarItem(
label = "Avisos",
icon = Icons.Outlined.Notifications,
selectedIcon = Icons.Filled.Notifications
),
NavigationBarItem(
label = "Perfil",
icon = Icons.Outlined.Person,
selectedIcon = Icons.Filled.Person
)
)
)
Cabe resaltar que, si tus ítems son creados a partir de las preferencias de usuario, entonces necesitas declararlos como StateFlow
en un View Model.
Y si sabes que los destinos perduraran estáticos hacia el futuro, entonces es posible crear una enumeración para definirlos en vez de una clase de datos.
Definir Destino Seleccionado
Tal como afirmamos al inicio, solo existe un destino seleccionado al tiempo, así que es necesario guardar el estado de dicho elemento a través de las recomposiciones con remember()
.
Así que definamos un estado mutable del tipo String
que inicie con el valor de "Inicio"
:
@Composable
fun NavigationBarScreen() {
// "Inicio" está en la posición 0
var selectedItem by remember { mutableStateOf(ItemsExample.items[0].label) }
Scaffold(
topBar = { TopBar() },
bottomBar = {
ExampleNavigationBar(
items = ItemsExample,
selectedItem = selectedItem // Estado de selección actual
)
}
) {
//..
}
}
Comunicaremos la selección actual a partir de un nuevo parámetro selectedItem, que nos permita actualizar el estado visual del destino.
Construir Navigation Bar
Regresa a NavigationBarExample
y añade a la firma el parámetro del estado de los items. Luego invoca a NavigationBar()
y en su lambda de contenido invoca a NavigationBarItem()
basado a partir del nuevo parámetro:
@Composable
private fun ExampleNavigationBar(
items: NavigationBarItems,
selectedItem: String? = null,
onItemClick: (String) -> Unit = {}
) {
NavigationBar {
items.items.forEach { item ->
NavigationBarItem(
selected = item.label == selectedItem,
onClick = { onItemClick(item.label) },
icon = {
Icon(
imageVector = item.icon,
contentDescription = null
)
},
label = { Text(item.label) },
)
}
}
}
Como ves, de NavigationBar()
solo usamos su parámetro content
, el cual es una lambda con recibidor RowScope
para distribuir los items en su interior de forma horizontal.
En su interior invocamos la función forEach
de la propiedad NavigationBarItems para invocar a NavigationBarItem. Los parámetros usados son:
selected
: Determina si el ítem está seleccionado. Usamos un operador de comparación entre la etiqueta de la iteración y el estado entranteicon
: Composable que especifica el icono del ítem. Asociamos a la propiedad NavigationBarItem.icon para crear un componente Iconlabel
: Etiqueta del ítem. Asociamos aNavigationBarItem.label
onClick
: Función llamada cuando el ítem es clickeado. En el siguiente apartado veremos el argumento que recibirá
Al revisar la Preview ya podrás ver la construcción de la barra de navegación como en la figura del inicio.
Procesar Tap En Destino
NavigationBar
Debido a que necesitamos que los componentes padres que ejecutan a NavigationBarExample
puedan capturar el evento en un ítem, añadimos un nuevo parámetro de tipo función llamado onItemClick
.
Para probar esta interacción, declararemos un estado con el ítem que está actualmente seleccionado y pasaremos como argumento una lambda que modifique su valor a la etiqueta que viene como parámetro:
@Composable
fun NavigationBarScreen2() {
var selectedItem by remember { mutableStateOf(ItemsExample.items[0].label) }
Scaffold(
//...
bottomBar = {
ExampleNavigationBar(
//...
onItemClick = { selectedItem = it }
)
}
) {
//...
}
}
Añadir Navegación
Aunque en el estado actual que tenemos el ejemplo podemos experimentar el uso de la Navigation Bar en Android, no es la forma en que la estaremos usando en nuestras Apps.
Necesitamos combinarla con navegación real entre nuestros destinos principales a través del Navigation Component. Por esta razón, expandiremos el código para incluir la navegación.
Crear Destinos De Navegación
Iniciaremos creando cada una de las pantallas que representan las rutas asociadas a los destinos en la Navigation Bar. No obstante, por cuestiones de simplicidad, serán pantallas básicas que muestren tan solo la ruta actual seleccionada.
Así que añade un nuevo archivo llamado NavigationExample.kt
y añade las rutas y funciones de extensión para los destinos:
@Serializable
object Home
@Serializable
object Explore
@Serializable
object Notifications
@Serializable
object Profile
/**
* Representa un destino de ejemplo. Normalmente creas un archivo Kotlin
* por cada uno de tus pantallas.
*/
@Composable
fun OtherDestination(route: String = "Default") {
Column(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(route, style = MaterialTheme.typography.displaySmall)
}
}
fun NavGraphBuilder.homeScreen() {
composable<Home> {
OtherDestination("Inicio")
}
}
fun NavGraphBuilder.exploreScreen() {
composable<Explore> {
OtherDestination("Explorar")
}
}
fun NavGraphBuilder.notificationsScreen() {
composable<Notifications> {
OtherDestination("Avisos")
}
}
fun NavGraphBuilder.profileScreen() {
composable<Profile> {
OtherDestination("Perfil")
}
}
Como bien sabes, normalmente creas un archivo de navegación con cada clase, pero debido a que esto es una explicación didáctica simple, las pondremos en un solo archivo.
El componente OtherDestination()
representa una pantalla genérica que reutilizaremos en los cuatro destinos. Solo tiene como estado el nombre de la ruta.
Crear NavHost
Lo siguiente es crear el contenedor de navegación para incluir a los destinos anteriores. Así que ve a NavigationBarScreen
y reemplaza el contenido actual por la invocación de NavHost
. En su interior irá la creación del grafo de navegación a partir de las extensiones que definimos en el paso anterior:
@Composable
fun NavigationBarScreenNav() {
val navController = rememberNavController()
Scaffold(
//...
) {
NavHost(navController, startDestination = Home, modifier = Modifier.padding(it)) {
homeScreen()
exploreScreen()
notificationsScreen()
profileScreen()
}
}
}
Observa que el argumento del parámetro builder
de NavHost
es la invocación de nuestras cuatro rutas de ejemplo.
Definir Destino Actual
En vista de que ahora tenemos rutas asociadas a los ítems, es necesario representar dicha propiedad en nuestra clase NavigationBarItem
. Las rutas son objetos o clases de datos con distintas referencias, por lo que introducimos un parámetro genérico en nuestra clase y añadimos un parámetro para la ruta:
data class NavigationBarItem <T: Any>(
//...
val route: T
)
Lo siguiente es reemplazar el estado que tenías anteriormente para hacerle seguimiento a la label seleccionada, por el objeto de destino actual en la back stack. Recuerda que este valor lo obtenemos con currentBackStackEntryAsState()
y la propiedad NavBackStackEntry.destination
.
@Composable
fun NavigationBarScreen() {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Scaffold(
//...
bottomBar = {
ExampleNavigationBar(
//...
items = ItemsExample,
selectedItem = currentDestination,
onItemClick = {
navController.navigate(it)
}
)
}
) {
//...
}
}
En consecuencia, modificaremos a ExampleNavigationBar para detecte la selección a partir de currentDestination:
@Composable
private fun ExampleNavigationBar(
items: NavigationBarItems,
selectedItem: NavDestination? = null,
onItemClick: (Any) -> Unit = {} // ← Cambia el parámetro por Any
) {
NavigationBar {
items.items.forEach { item ->
val selected = selectedItem.isRoute(item.route) // ← Determina selección
NavigationBarItem(
selected = selected,
//...
)
}
}
}
fun NavDestination?.isRoute(route: Any) = this?.hasRoute(route::class) == true
Con estos elementos implementados, cuando ejecutes la App o vayas a modo interacción verás como se navega al destino apropiado.
A pesar de eso, con el código actual, la cantidad de destinos añadidos a la back stack continúa incrementando sin cesar. Situación que debe ser solucionada…
Evitar Destinos Duplicados En El NavHost
Por motivos de rendimiento y congruencia, cada que navegamos a un destino de la Navigation Bar, necesitamos que solo ese sea conservado en la stack del navegador. Afortunadamente tenemos el tipo NavOptions
para configurar nuestra navegación y asegurar este comportamiento.
@Composable
fun NavigationBarScreen() {
//...
Scaffold(
//...
bottomBar = {
ExampleNavigationBar(
//...
onItemClick = {
navController.navigate(it) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
) {
//...
}
}
Del código anterior debes saber que:
- Usamos
popUpTo()
para desapilar el destino inicial junto a todos los demás elementos que no coincidan con el - La propiedad
launchSingleTop
evita la duplicación de un destino - La propiedad
restoreState
restaura el estado del destino guardado consaveState
. Esto es de utilidad si has alterado un destino y deseas conservar dicho cambio a través de la navegación
De esta forma, cuando ejecutes el aplicativo la navegación solo mantendrá al destino actual en memoria.
Personalizar Navigation Bar
Seguidamente, veremos casos de uso particulares que en ocasiones te vendrán de utilidad.
Añadir Badge Pequeña A Destino
En el caso en que desees expresar el cambio de estado o una nueva notificación en algún ítem de la Navigation Bar, usa una Badge pequeña para remarcar el detalle de forma visual.
Para representar esta característica, añadiremos una nueva sealed class llamada BadgeType
con tres tipos: Ítem sin badge, ítem con indicador circular e ítem con indicador numerado:
sealed class BadgeType {
data object None : BadgeType()
data object Circle : BadgeType()
data class Numbered(val number: Int) : BadgeType() {
init {
require(number in 0..99) {
"El número de notificaciones debe estar entre 0 y 99"
}
}
val numberString = number.toString()
}
}
Luego añadiremos una propiedad de BadgeType
a NavigationBarItem
:
data class NavigationBarItem <T: Any>(
//...
val badge: BadgeType = BadgeType.None,
)
Acto seguido modifica la declaración del ítem de Perfil para que tenga una badge pequeña:
val ItemsExample = NavigationBarItems(
items = listOf(
//...
NavigationBarItem(
label = "Perfil",
icon = Icons.Outlined.Person,
selectedIcon = Icons.Filled.Person,
badge = BadgeType.Circle, // ← Badge pequeña
route = Profile
)
)
)
Posteriormente, ve a ExampleNavigationBar
y extrae como función el argumento de icon
que usamos en NavigationBarIcon()
.
@Composable
private fun ExampleNavigationBar(
//...
) {
NavigationBar {
state.items.forEach { item ->
val selected = item.label == selectedItem
NavigationBarItem(
//...
icon = {
NavigationBarItemIcon(
isSelected = selected,
badge = item.badge,
selectedIcon = item.selectedIcon,
unselectedIcon = item.unselectedIcon
)
},
)
}
}
}
La idea es que el nuevo componente NavigationBarItemIcon()
reciba una bandera para determinar si el item está seleccionado, el BadgeType
y los iconos. Por supuesto usaremos al componente Badge()
en los casos correspondientes:
@Composable
fun NavigationBarItemIcon(
isSelected: Boolean,
badge: BadgeType,
selectedIcon: ImageVector,
unselectedIcon: ImageVector
) {
val iconContent: @Composable () -> Unit = {
Icon(
imageVector = if (isSelected) selectedIcon else unselectedIcon,
contentDescription = null
)
}
when (badge) {
BadgeType.None -> iconContent()
BadgeType.Circle -> {
BadgedBox(badge = { Badge() }) {
iconContent()
}
}
is BadgeType.Numbered -> {
BadgedBox(badge = {
Badge {
Text(text = badge.numberString)
}
}) {
iconContent()
}
}
}
}
Con este código completo, puedes ejecutar el aplicativo y verificar que el resultado sea como el de la ilustración.
Añadir Badge Grande A Destino
Por otro lado, si lo que deseas es añadir una badge con numeración para indicar que un estado cuantificable del destino ha cambiado, entonces usa el tipo personalizado BadgeType.Numered
que declaramos previamente.
Por ejemplo, la ilustración anterior muestra el número 10 en el ítem Avisos. Esto podemos lograrlo modificando la instancia de NavigationBarItem
como sigue:
val ItemsExample = NavigationBarItems(
items = listOf(
//...
NavigationBarItem(
label = "Avisos",
unselectedIcon = Icons.Outlined.Notifications,
selectedIcon = Icons.Filled.Notifications,
badge = BadgeType.Numbered(10),
route = Notifications
)
)
)
Comportamiento De Navigation Bar
Animar Indicador De Actividad
Existe la posibilidad de modificar el tipo de foco que se le da a la selección de un destino en la Navigation Bar a través de una animación donde aparece la etiqueta y desplaza al icono.
Afortunadamente este efecto podemos lograrlo sin mucho esfuerzo, desde la misma función NavigationBarItem
a través del parámetro alwaysShowLabel
. Este determinar si la etiqueta debe ser mostrada siempre o solo cuando el ítem está seleccionado.
@Composable
private fun ExampleNavigationBar(
/...
) {
NavigationBar {
items.items.forEach { item ->
NavigationBarItem(
//...
alwaysShowLabel = false // ← Activa animación
)
}
}
}
Re-seleccionar Destino Activo Actual
Cuando se selecciona el destino seleccionado actualmente, Google sugiere desplazar la posición del scroll hacia la parte superior de la pantalla. Esta acción mejora la experiencia del usuario al permitirle volver rápidamente al inicio del contenido, especialmente útil en listas largas o feeds.
Supongamos que nuestro home ahora es un texto de 500 palabras que requiere desplazamiento vertical para ser leído:
private val SampleText = LoremIpsum(500).values.joinToString("")
fun NavGraphBuilder.homeScreen(scrollState: ScrollState) {
composable<Home> {
Box(
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxSize()
) {
Text(text = SampleText)
}
}
}
Cuando detectemos desde NavigationBarScreen
que el ítem actual se ha re-seleccionado, entonces usaremos animateScrollTo(0)
para retornar el contenido a la parte superior.
@Composable
fun NavigationBarScreenNav() {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val homeScrollState = rememberScrollState()
val scope = rememberCoroutineScope()
Scaffold(
//...
bottomBar = {
ExampleNavigationBar(
//...
onItemClick = {
if (currentDestination.isRoute(Home) && it == Home) {
scope.launch {
homeScrollState.animateScrollTo(0)
}
return@ExampleNavigationBarNav
}
//...
}
)
}
) {
NavHost(navController, startDestination = Home, modifier = Modifier.padding(it)) {
homeScreen(homeScrollState)
//...
}
}
}
Conclusión
En este tutorial, hemos explorado paso a paso cómo crear una Navigation Bar en Android usando Jetpack Compose, desde su configuración inicial hasta su personalización avanzada. Aprendiste:
- Qué es una Navigation Bar y cuándo usarla en aplicaciones móviles para una navegación intuitiva.
- Cómo configurar un proyecto en Android Studio y definir las dependencias necesarias para la navegación.
- Cómo diseñar la estructura básica usando
NavigationBar
yNavigationBarItem
, junto con la gestión de estados para el ítem seleccionado. - Cómo integrar Navigation Component para una navegación fluida entre pantallas, evitando duplicación de destinos en la back stack.
- Técnicas de personalización, como añadir badges (indicadores de notificaciones) y animaciones para mejorar la experiencia de usuario.
- Comportamientos avanzados, como el scroll automático al re-seleccionar un destino activo.
Con estos conocimientos, podrás implementar una Navigation Bar profesional y adaptable en tus aplicaciones Android, garantizando una navegación clara y consistente.
¿Estás Creando Una App De tareas?
Te comparto una plantilla Android profesional con arquitectura limpia, interfaz moderna y funcionalidades listas para usar. Ideal para acelerar tu desarrollo.

Únete Al Discord De Develou
Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!