Navigation Compose vs. un setup de una sola Activity con múltiples Fragments — ¿qué cambió?
Fragments, el XML del nav graph y Safe Args resolvieron durante una década lo de 'una sola Activity, muchas pantallas' — Navigation Compose reemplaza el grafo, los argumentos y hasta el ciclo de vida de Fragment por funciones y rutas tipadas.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Navigation Compose vs. un setup de una sola Activity con múltiples Fragments — ¿qué cambió?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#El problema que ambos enfoques resuelven
Antes de que existiera cualquiera de los dos, muchas apps usaban una Activity por pantalla, lo que significaba que cada transición de pantalla era una transacción completa de Activity — su propia ventana, su propio Intent, su propio onCreate, y el estado compartido pasaba por extras del Intent (lo que implicaba Parcelable/Serializable por todos lados) o por un singleton. Eso es lento, pesado en memoria e incómodo para UI compartida como una barra de navegación inferior o un toolbar persistentes.
La arquitectura de una sola Activity con múltiples Fragments (Jetpack Navigation Component, más o menos desde 2018) arregló eso: una Activity hospeda un NavHostFragment, y las pantallas se convierten en Fragments que se intercambian dentro de ese host. Un recurso nav_graph.xml declara cada destino y las acciones entre ellos, y el plugin de Gradle de Navigation genera clases de argumentos tipadas (Safe Args) para que no pases un Bundle crudo y esperes que el Fragment receptor lea las claves correctas.
<!-- nav_graph.xml --><fragmentandroid:id="@+id/homeFragment"android:name="com.example.app.HomeFragment"><actionandroid:id="@+id/action_home_to_profile"app:destination="@id/profileFragment" /></fragment><fragmentandroid:id="@+id/profileFragment"android:name="com.example.app.ProfileFragment"><argument android:name="userId" app:argType="string" /></fragment>
// Safe Args generado, en acciónval action = HomeFragmentDirections.actionHomeToProfile(userId = clickedId)findNavController().navigate(action)
Eso fue una mejora real — un solo back stack, un solo NavController, deep links declarados en el mismo grafo — pero seguía tratando cada pantalla como un Fragment, con el ciclo de vida completo de Fragment (onCreate, onCreateView, onViewCreated, onDestroyView — noten que no onDestroy, que es separado del ciclo de vida de la instancia del Fragment) y sus problemas conocidos: referencias a vistas que hay que anular en onDestroyView para no filtrar la jerarquía de vistas vieja, viewLifecycleOwner versus el ciclo de vida propio del Fragment para observers de LiveData, y transacciones de FragmentManager que podían tirar IllegalStateException si se hacían commit después de onSaveInstanceState.
#Qué cambia realmente Navigation Compose
Navigation Compose (parte de Jetpack Compose, estándar desde aproximadamente 2021, con rutas tipadas desde Navigation 2.8 en 2024) mantiene la misma idea central — un NavController, un back stack, un grafo de destinos — pero ahora cada destino es una función @Composable, no un Fragment. Ese único cambio elimina toda una categoría de bugs, porque los composables no tienen una "vista" separada de la composición que se pueda filtrar, y la recomposición (no un ciclo de vida de vista en dos fases) gobierna qué se muestra en pantalla.
@Serializableobject HomeRoute@Serializabledata class ProfileRoute(val userId: String)@Composablefun AppNavHost(navController: NavHostController = rememberNavController()) {NavHost(navController = navController, startDestination = HomeRoute) {composable<HomeRoute> {HomeScreen(onUserClick = { id -> navController.navigate(ProfileRoute(userId = id)) })}composable<ProfileRoute> { backStackEntry ->val route: ProfileRoute = backStackEntry.toRoute()ProfileScreen(userId = route.userId)}}}
Dos cosas reemplazan lo que Safe Args generaba antes por vos: las rutas son tipos de Kotlin @Serializable comunes (usando kotlinx.serialization), así que ProfileRoute(userId = id) es el argumento tipado — sin paso de codegen, sin un recurso XML separado que mantener sincronizado con la clase del Fragment. Y backStackEntry.toRoute<ProfileRoute>() lo deserializa de vuelta del lado receptor, completamente tipado, con un error de compilación si la forma alguna vez se desincroniza.
#Qué se mantuvo conceptualmente igual
Vale la pena ser precisos con esto en una entrevista, porque los conceptos casi no cambiaron aunque la API sí:
- Los grafos anidados siguen existiendo —
navigation<GraphRoute>(startDestination = ...) { ... }reemplaza la etiqueta XML<navigation>, para agrupar un flujo (por ejemplo, onboarding) detrás de su propio subgrafo. - El scoping de ViewModel a una entrada de navegación sigue funcionando igual —
hiltViewModel()dentro de un destino composable te da un ViewModel con scope a esa entrada del back stack, el análogo directo de un ViewModel con scope a Fragment. - Los deep links se siguen declarando por destino (
deepLinks = listOf(navDeepLink { uriPattern = "..." })), y el back stack,popUpTo,saveState/restoreStatepara cambiar de tab estilo bottom-nav, y los back stacks múltiples se mantienen conceptualmente sin cambios. - La migración es incremental, no todo o nada:
ComposeViewte deja incrustar un composable dentro de un Fragment existente, yAndroidViewdeja que una pantalla Compose hospede un widget legacy basado enView— la mayoría de las migraciones reales corren ambos sistemas de navegación en paralelo por un tiempo, con unaActivityhospedando ya sea unNavHostFragmentbasado en Fragments para pantallas legacy o unNavHostde Compose para las nuevas (rara vez ambos grafos comparten un solo back stack, porque eso requiere un puente deliberado).
🧠 Check your understanding
0/2 · 0/2 answered1. En Navigation Compose, ¿qué reemplaza a las clases de argumentos generadas por Safe Args?
2. ¿Por qué Navigation Compose evita la clásica categoría de bugs de 'referencia a vista filtrada después de onDestroyView' tan común en Fragments?