Una pieza fundamental en el desarrollo de nuestras apps es la navegación, es por ello que hoy aprenderemos a implementar la navegación en Kotlin Multiplatform con Voyager.

Voyager para navegar

La gestión de la navegación puede ser compleja si no se gestiona bien. Es por ello que siempre que como norma general se suele utilizar una librería que nos gestiona todo el flow ya que aunque no lo parezca, necesitamos una gestión de la pila de navegación, algún mecanismo para pasar parámetros y mucho más que a simple vista no se aprecia.

En esta ocasión utilizaremos Voyager, la librería más recomendada a día de hoy para navegar con Kotlin Multiplatform. Entre sus features destacaría las siguientes:

  • Multiplataforma: Soporta iOS, Android, escritorio y web.
  • Distintos tipos de navegación: Linear, BottomSheet, TabNavigation y anidada.
  • Multimódulo.
  • Integración con ViewModel.
  • Gestión de los deeplinks.
  • Callbacks del ciclo de vida.
  • Captura y gestiona «automágicamente» el botón de atrás.

Añadiendo librerías en Kotlin Multiplatform

El primer paso será preparar nuestro proyecto. Para ello iremos al wizard de KMP y crearemos un proyecto exactamente igual como hicimos en el capítulo anterior.

Una vez configurado y abierto el proyecto es el turno de entender nuestro nuevo gradle. Para ello, y una vez hayamos puesto la vista de proyecto en Android Studio tendremos que ir al fichero build.gradle.kts que está dentro del directorio composeApp.

Ruta del gradle.
Ruta del gradle.

Una vez dentro veremos que nuestro gradle es bastante distinto a cualquiera que hayamos visto en Android, ya que trae muchísimas más configuraciones (es normal, ahora tenemos muchos proyectos aquí dentro). Pero vamos a ir tan poco a poco que verás que es muy sencillo.

Lo primero que haremos será buscar la etiqueta kotlin{}, dentro tendremos un sourceSets donde veremos androidMain.dependencies, desktopMain.dependencies y commonMain.dependencies.

Esto ahora mismo es lo único que nos interesa, pues se trata de la gestión de dependencias para cada plataforma (iOS va por su lado, como siempre). Tenemos la posibilidad de añadir dependencias específicas para cada plataforma, pero la gracia es usar siempre que podamos dependencias genéricas, ya que nos aseguran que no tendremos que tocar cada entorno. Entonces, si tenemos una librería multiplataforma como es el caso, la tendremos que añadir en commonMain.dependencies para que se añada a todos los proyectos de la app.

Version catalog

Seguramente te extrañe no ver en ningún implementation las url de las dependencias en el gradle y esto es porque la forma de añadir dependencias en este proyecto viene dada con Version catalog.

Version catalog nos ayuda a gestionar las dependencias de una forma más escalable y sencilla, pero no quiero hacer mucho hincapié aquí ya que todavía no lo he explicado en capítulos ni vídeos anteriores (si queréis vídeo solo tenéis que pedirlo) por lo que no voy a entrar mucho en detalle.

Para añadir las versiones de Voyager de esta forma tendremos que ir a gradle/libs.version.toml. Una vez dentro verás que están las definiciones de todas las librerías del proyecto. Nosotros, dentro de [libraries] tendremos que añadir dos dependencias de Voyager.

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

Y en [versions]:

voyager = "1.0.0-rc10"

Navegación en KMP

La navegación como tal es muy sencilla, solo tendremos que crear una especie de clases que serán las contenedoras de nuestros composable.

@Composable
fun App() {
    MaterialTheme {
       Content()
    }
}

@Composable
fun Content() {
    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = { }) {
            Text("Navegación básica")
        }
    }
}

Esta es la función principal que se usa en todas las apps, básicamente es un composable que llama a una función content() que es otro composable con el contenido que queramos, da igual.

Para habilitar la navegación solo tendremos que añadir el contenido de nuestra pantalla (en este caso la función content) en una especie de clase que extenderá de Screen.

class MainScreen : Screen {
    @Composable
    override fun Content() {
       Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
           Button(onClick = { }) {
                Text("Navegación básica")
           }
       }
   }
}

Ahora nuestra función principal ( App() ) tendrá que llamar a nuestra «pantalla» desde un objeto navigator.

@Composable
fun App() {
    MaterialTheme {
        Navigator(screen = MainScreen())
    }
}

Este navigator nos va a gestionar todo el flujo de la navegación de manera automática.

Navegar entre pantallas

Una vez configurado todo lo anterior la navegación por pantallas es añadir una línea. Lo único que tenemos que tener en cuenta es que cada pantalla que queramos habrá que meterlo dentro de una clase de tipo Screen. Por ejemplo vamos a crear otra pantalla llamada SecondScreen.

class SecondScreen : Screen {

    @Composable
    override fun Content() {
        Column(
            modifier = Modifier.fillMaxSize().background(Color.Blue),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Segunda Pantalla", fontSize = 26.sp, color = Color.White)
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = {  }) { Text("Volver") }
        }
    }

}

Esta pantalla solo tiene un título y un botón atrás que ahora mismo no hace nada.

Para poder navegar tendremos que acceder al objeto navigator. Este objeto lo podremos recuperar dentro de cualquier función Content de cualquier clase de tipo Screen.

val navigator = LocalNavigator.currentOrThrow

Cuando llamamos a LocalNavigator.currentOrThrow le estamos diciendo que nos de un objeto Navigator existente o que lance una excepción, ya que siempre debería existir. Si nos da miedo que la aplicación reviente podemos usar solo LocalNavigator.current pero así tendremos un objeto Navigator? (nulable) y tendremos que estar comprobando todo el rato.

Ahora para navegar solo tendríamos que llamar a nuestro navigator.push() y pasarle por parámetro la Screen a la que queramos navegar. Por ejemplo la primera pantalla quedaría así.

class MainScreen : Screen {

    @Composable
    override fun Content() {
        val navigator = LocalNavigator.currentOrThrow
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            Button(onClick = {
                navigator.push(SecondScreen())
            }) {
                Text("Navegación básica")
            }
        }
    }
}

Y la segunda Screen es exactamente igual pero llamamos a navigator.pop() que lo que hará será volver a la pantalla anterior.

class SecondScreen : Screen {

    @Composable
    override fun Content() {
        val navigator = LocalNavigator.currentOrThrow
        Column(
            modifier = Modifier.fillMaxSize().background(Color.Blue),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Segunda Pantalla", fontSize = 26.sp, color = Color.White)
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = { navigator.pop() }) { Text("Volver") }
        }
    }

}
Navegación KMP
Navegación entre las dos pantallas.

Transiciones

Si recuerdas al añadir las dependencias metimos la de transiciones así que es el momento de utilizarlas. Una transición es simplemente una animación que se aplicar al cambiar de pantalla otorgando un efecto más atractivo y evitando ese cambio de pantalla tan «brusco» que viene por defecto.

Podemos utilizar transiciones que nos traen por defecto (tres en este caso) o podemos crear las nuestras propias. Para este ejemplo utilizaremos las tres que vienen.

@Composable
fun App() {
    MaterialTheme {
        Navigator(screen = MainScreen()) { navigator ->
            SlideTransition(navigator)
        }
    }
}

Lo único que hemos hecho es ir al Navigator y abrir una función lambda la cual va a recibir un SlideTransition() que es una de las clases genéricas de navegación que nos trae la librería. Si ejecutamos y navegamos podemos ver la diferencia perfectamente.

También disponemos de FadeTransition() y ScaleTransition().

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.