Noticia Guía Completa de Navegación Avanzada en Jetpack Compose

Navegación Avanzada en Jetpack Compose


Si te has lanzado a la aventura de desarrollar con Jetpack Compose, habrás notado que no solo cambia la interfaz, sino que trastoca por completo la forma en la que entendemos el flujo de una aplicación. Olvida los mundos complicados de los Fragmentos tradicionales; aquí nos movemos en un ecosistema donde la navegación se basa en funciones componibles que se intercambian dinámicamente.

Aprender a movernos entre pantallas no es moco de pavo, ya que implica gestionar un estado global que determine qué ve el usuario en cada momento. En este artículo vamos a desgranar desde los conceptos más básicos hasta las técnicas más avanzadas, como el paso de parámetros complejos y la integración de enlaces externos, para que tu app sea robusta y escalable.

Los pilares de Navigation Compose​


Para empezar a montar el tinglado, debemos entender que el sistema se apoya en tres patas fundamentales. Primero tenemos el NavController, que es el cerebro encargado de ejecutar los saltos entre pantallas y gestionar la pila de actividades. Luego está el NavGraph, que básicamente es el mapa donde definimos qué rutas existen y a qué componente se asocian. Por último, el NavHost actúa como el contenedor físico que renderiza la pantalla actual según la ruta activa.

Para poner esto en marcha, es vital utilizar rememberNavController() en el nivel más alto de la jerarquía, normalmente en la Activity principal. Esto asegura que el controlador sobreviva a los cambios de configuración y actúe como la fuente de verdad para toda la navegación de la aplicación, integrándose correctamente con el ciclo de vida de una aplicación de Android.

Definición de rutas y gestión del NavHost​


Una ruta es, sencillamente, una cadena de texto que identifica un destino, muy parecido a cómo funcionan las URLs en la web. Para evitar errores de escritura y mantener el código limpio, una práctica muy recomendada es utilizar clases de tipo enum. De este modo, podemos asociar cada ruta a un valor concreto y, si queremos ponernos más detallistas, añadir propiedades como el título de la pantalla directamente en el enum.

El NavHost se configura indicando el controlador y el destino inicial. Dentro de su bloque, utilizamos la función composable() para mapear cada ruta con su respectiva pantalla. Un punto clave aquí es la reutilización de componentes; por ejemplo, si tienes varias pantallas con el mismo diseño pero distintos datos (como una selección de sabores y otra de fechas), puedes usar la misma función componible pasando diferentes parámetros.

Navegación inteligente y flujo de datos​


Uno de los errores más comunes es pasar el NavController a todas las pantallas. ¡Ni hablar! Eso rompe la reutilización y complica las previsualizaciones. Lo ideal es elevar la lógica de navegación: las pantallas deben recibir lambdas o callbacks (como un onNextButtonClicked) y dejar que el NavHost decida a dónde ir. Así, la IU es independiente de la lógica de negocio.

Pasando argumentos entre pantallas​


Cuando necesitamos que una pantalla sepa qué elemento mostrar (por ejemplo, el detalle de un producto), utilizamos argumentos en la ruta. Esto se hace añadiendo el parámetro entre llaves, como "detalle/{id}". Podemos definir el tipo de dato, como NavType.IntType, para asegurar que la app no pete al recibir información inesperada.

Para recuperar este dato, accedemos al NavBackStackEntry dentro del NavHost. Desde ahí, extraemos el valor usando arguments?.getString() o el método correspondiente al tipo de dato, pasándolo finalmente como parámetro al componente de la pantalla de detalle.

Optimización de la pila de actividades y UX​


Si no controlamos la pila de actividades, el usuario podría terminar con diez copias de la misma pantalla al pulsar repetidamente un botón. Para evitar este caos, usamos el parámetro launchSingleTop = true, que garantiza que solo haya una instancia del destino en la cima. Además, el uso de popUpTo permite limpiar la pila hasta un punto concreto, evitando que el botón de retroceso del sistema se vuelva infinito.

Para que la experiencia sea fluida, podemos activar restoreState = true. Esto permite que, si el usuario navega fuera de una pantalla y luego vuelve, se conserve la posición del scroll y los datos introducidos, evitando que la pantalla se recargue desde cero y dando una sensación de mayor velocidad.

Integración de Deep Links e Intents externos​


La navegación no tiene por qué limitarse al interior de la app. Los Deep Links permiten que un enlace externo (como un email o una notificación) abra una pantalla específica. Esto requiere configurar un intent-filter en el archivo AndroidManifest.xml definiendo el esquema y el host (por ejemplo, rally://cuenta).

En el NavHost, añadimos la propiedad deepLinks al destino correspondiente usando navDeepLink. Así, el sistema reconoce la URL y redirige al usuario automáticamente. Por otro lado, para interactuar con otras aplicaciones del sistema (como compartir un pedido), utilizamos los Intents de Android, específicamente ACTION_SEND, configurando el tipo de contenido como texto plano y lanzando el chooser del sistema.

Sincronización de la AppBar con la navegación​


Para que el usuario no se pierda, la barra superior debe reaccionar al cambio de pantalla. Podemos lograr esto observando el estado actual de la navegación mediante currentBackStackEntryAsState(). Al obtener la ruta activa, podemos actualizar el título de la TopAppBar dinámicamente.

Es fundamental implementar el botón Up (la flecha de volver). Este solo debe ser visible si existe una pantalla previa en la pila, lo cual comprobamos verificando que previousBackStackEntry no sea nulo. Al pulsarlo, ejecutamos navController.navigateUp() para regresar un paso atrás de forma natural.

Estrategias de Testing para la navegación​


Probar la navegación puede ser un quebradero de cabeza si no se hace bien. La clave es usar la librería navigation-testing y el TestNavHostController. En lugar de probar la Activity completa, creamos un entorno de prueba donde instanciamos el NavHost y verificamos que, tras un clic, el nodo con el contentDescription de la pantalla de destino esté visible.

También es muy útil comparar la ruta actual del controlador con la ruta esperada usando aserciones de JUnit. Esto nos permite validar flujos complejos sin necesidad de interactuar visualmente con cada elemento, asegurando que la lógica de enrutamiento sea sólida antes de desplegar la app a producción.

Dominar el ecosistema de navegación en Compose implica pasar de un esquema rígido de actividades a un flujo flexible basado en estados y rutas. Desde la configuración básica del NavHost y el uso de enums para evitar errores, hasta la implementación de argumentos dinámicos, Deep Links y una gestión eficiente de la pila de actividades con launchSingleTop, el objetivo es siempre el mismo: crear una experiencia de usuario intuitiva y un código mantenible que facilite las pruebas automatizadas y la escalabilidad del proyecto. Comparte la guía y más usuarios conocerán del tema.

Continúar leyendo...