Seguimos con la navegación y en este capítulo aprenderemos a implementar el Bottom Bar Navigation en nuestras apps.

Bottom Navigation con Voyager

Para la navegación inferior volveremos a utilizar Voyager, la que para mi gusto es a día de hoy la mejor dependencia para la navegación de Kotlin Multiplataforma.

Vamos a seguir trabajando con el mismo proyecto del capítulo anterior por lo que te recomiendo que lo veas y si quieres descargarte el proyecto lo tienes completo en el repositorio de GitHub.

El primer paso será volver al fichero libs.versions.toml y añadir la nueva dependencia.

voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }

Acto seguido habrá que añadir la nueva dependencia en commonMain.dependencies del fichero build.gradle.kts.

commonMain.dependencies {
    implementation(libs.voyager.navigator)
    implementation(libs.voyager.transitions)
    implementation(libs.voyager.tabNavigator)
    implementation(compose.runtime)
    implementation(compose.foundation)
    implementation(compose.material)
    @OptIn(ExperimentalComposeLibrary::class)
    implementation(compose.components.resources)
}

Esa es toda la configuración necesaria.

Creando los tabs

Normalmente en Android utilizaríamos fragments para mostrar cada una de las pestañas o tabs del bottom navigation, en KMP lo que tenemos que usar son clases u objetos que extiendan de Tab.

object HomeTab : Tab {

    override val options: TabOptions
        @Composable
        get() {
            val icon = rememberVectorPainter(Icons.Default.Home)
            return remember {
                TabOptions(
                    index = 0u,
                    title = "Home",
                    icon = icon
                )
            }
        }

    @Composable
    override fun Content() {
        Box(Modifier.fillMaxSize().background(Color.Green), contentAlignment = Alignment.Center) {
            Text("HomeScreen", fontSize = 22.sp, color = Color.Black)
        }
    }
}

Analicemos el código anterior. Para empezar en lugar de una clase estoy usando un objeto ya que como solo voy a usar HomeTab para un único lugar con tener un Singleton es suficiente (pero puedes usar una clase normal si esto te confunde).

Nuestra clase tendrá que sobre escribir la variable options y la función content.

Options será la variable encargada de configurar el tab, es decir, la que asignará el nombre, el icono y el índice. También fíjate que el índice lleva una u al final ya que es un UShort.

Content será el método encargado de mostrar todo el contenido de esa pestaña. En esta ocasión cada tab tendrá una Box que ocupe todo el tamaña con un texto y un color de fondo.

El siguiente paso será crear dos vistas más para tener al menos tres tabs.

object FavTab  : Tab {

    override val options: TabOptions
        @Composable
        get() {
            val icon = rememberVectorPainter(Icons.Default.Favorite)
            return remember {
                TabOptions(
                    index = 1u,
                    title = "Fav",
                    icon = icon
                )
            }
        }

    @Composable
    override fun Content() {
        Box(Modifier.fillMaxSize().background(Color.Yellow), contentAlignment = Alignment.Center) {
            Text("FavScreen", fontSize = 22.sp, color = Color.Black)
        }
    }
}
object ProfileTab : Tab {

    override val options: TabOptions
        @Composable
        get() {
            val icon = rememberVectorPainter(Icons.Default.Menu)
            return remember {
                TabOptions(
                    index = 2u,
                    title = "Profile",
                    icon = icon
                )
            }
        }

    @Composable
    override fun Content() {
        Box(Modifier.fillMaxSize().background(Color.Gray), contentAlignment = Alignment.Center) {
            Text("ProfileScreen", fontSize = 22.sp, color = Color.White)
        }
    }
}

Preparando el Scaffold

Todas estas tabs que hemos creado tendrán que ser gestionadas por la clase que cargue todo el contenido, es decir, necesitamos una pantalla/composable que tenga la bottom bar que cargará los tabs. Para ello utilizaremos el scaffold, un componente que nos simplifica la gestión de vistas que ya que actúa como «esqueleto» de la pantalla y le podemos pasar varios componentes y él se encargará de colocarlo todo en su sitio.

class BottomBarScreen : Screen {

    @OptIn(ExperimentalVoyagerApi::class)
    @Composable
    override fun Content() {
        TabNavigator(
            HomeTab,
            tabDisposable = {
                TabDisposable(
                    it,
                    listOf(HomeTab, FavTab, ProfileTab)
                )
            }
        ) {
            Scaffold(
                topBar = {
                },
                bottomBar = {
                },
                content = { 
                    
                }
            )
        }
    }

}

La función Content() es la encargada de mostrar toda la vista de la pantalla. Su único componente interno será un TabNavigator() que será parte de la librería de Voyager.

Este componente recibirá primero el tab inicial, en este caso HomeTab. Luego el TabDisposable será el encargado de gestionar las tabs por lo que habrá que pasarle it (que en este caso es el propio TabNavigator y luego un listado con todos los tabs que queremos usar. Dentro de este componente es donde añadiremos el Scaffold.

Para no complicarlo mucho vamos a ir completando el Scaffold por partes.

topBar = {
    TopAppBar(title = { Text(it.current.options.title) })
}

Añadimos una pequeña Toolbar, que mostrará el título del tab seleccionado. Para hacer esto es muy sencillo, solo hace falta llamar a it (el TabNavigator) y al seleccionar current podemos acceder a toda la información de la tab seleccionada.

bottomBar = {
    BottomNavigation {
        val tabNavigator = LocalTabNavigator.current

        BottomNavigationItem(
            selected = tabNavigator.current.key == HomeTab.key,
            label = { Text(HomeTab.options.title) },
            icon = {
                Icon(
                    painter = HomeTab.options.icon!!,
                    contentDescription = null
                )
            },
            onClick = { tabNavigator.current = HomeTab }
        )


        BottomNavigationItem(
            selected = tabNavigator.current.key == FavTab.key,
            label = { Text(FavTab.options.title) },
            icon = {
                Icon(
                    painter = FavTab.options.icon!!,
                    contentDescription = null
                )
            },
            onClick = { tabNavigator.current = FavTab }
        )


        BottomNavigationItem(
            selected = tabNavigator.current.key == ProfileTab.key,
            label = { Text(ProfileTab.options.title) },
            icon = {
                Icon(
                    painter = ProfileTab.options.icon!!,
                    contentDescription = null
                )
            },
            onClick = { tabNavigator.current = ProfileTab }
        )

    }
}

Ahora estamos creando la parte visual del Bottom Bar. Lo único que hacemos es crear un composable BottomNavigatorItem por cada tab que queramos mostrar. Antes de crearlos hemos declarado una variable tabNavigator ya que la usaremos para comprobar cual es el tab seleccionado y para que cuando pulsemos en uno, marcarlo como seleccionado.

content = { CurrentTab() }

Cerramos con el content que simplemente tendremos que llamar a la función de Voyager CurrentTab() y esta se encargará de mostrar el contenido de la tab seleccionada. Ya solo queda probarlo en cualquiera de las plataformas que utilicemos, en esta caso lo ejecutaré en un iPhone 14.

Recuerda que puedes descargar TODO el proyecto desde GitHub de manera gratuita (y me ayudas mucho dándole una estrella ⭐️).


Si te ha gustado puedes seguirme en mis redes sociales en Aristi.Dev. Y si tienes dudas con este o cualquier otro artículo del blog únete al Discord de la comunidad y te ayudaremos.