NavController es el componente central de la biblioteca Navigation Compose que gestiona la pila de navegación y el estado del back stack en aplicaciones Android. A través de NavController se realizan transiciones entre pantallas, regreso a páginas anteriores y transferencia de datos entre rutas. Según Android Developers (2025), NavController es un elemento obligatorio de cualquier aplicación Compose con más de una pantalla. El controlador se crea mediante rememberNavController(), se pasa a NavHost y está disponible para llamar a navigate() desde cualquier punto de la composición. El soporte integrado de SavedStateHandle guarda automáticamente el estado de ViewModel durante la reconfiguración.
Puntos Clave
NavController es una clase de la biblioteca Navigation Compose que implementa el controlador de navegación para aplicaciones Compose. NavController gestiona la pila NavBackStackEntry, donde cada entrada contiene la ruta, los argumentos y el estado de la pantalla. El controlador admite operaciones básicas de navegación: transición, regreso, reemplazo y limpieza.
A diferencia del sistema View, donde la navegación se realizaba a través de FragmentManager o Intent, NavController funciona exclusivamente en el contexto de Compose. El back stack se almacena como un grafo NavDestination en lugar de una pila de Fragment. Esto elimina la sobrecarga de crear y destruir Fragment y simplifica las pruebas: NavController se puede simular mediante TestNavHostController.
NavController está estrechamente vinculado a NavHost, un contenedor que renderiza la pantalla actual del grafo. Sin NavHost, NavController no puede mostrar funciones composables pero conserva la capacidad de gestionar la pila. En una arquitectura típica, NavController se crea a nivel de Activity o composable principal y se pasa hacia abajo en el árbol de composición mediante parámetros.
Según Google, NavController ha pasado por varias versiones importantes. La versión 2.8.0 agregó Type-Safe Navigation, la versión 2.9.0 agregó soporte para predictive back gesture (Android 14+). El controlador es compatible con Material3 Scaffold y BottomNavigation. Para proyectos multimódulo, NavController se pasa a través de DI (Hilt/Koin) o parámetros del constructor.
NavController se crea mediante la función composable rememberNavController(). La función devuelve una instancia de NavHostController (subclase de NavController) vinculada al ciclo de vida del composable actual. Al salir de la composición, el controlador se limpia. Para preservar el controlador durante la reconfiguración, use rememberSaveable o ViewModel.
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "main"
) {
composable("main") { MainScreen(navController) }
composable("details") { DetailsScreen(navController) }
}
}
La configuración de NavController incluye: NavHostController (principal), TestNavHostController (pruebas) y ScopedNavController (hijo para grafos anidados). Para BottomNavigation, NavController debe ser único para toda la aplicación — crear un nuevo controlador en cada pestaña provocará pérdida de la pila. Para pasar el controlador a pantallas anidadas, use un parámetro de función en lugar de CompositionLocalProvider para mantener la legibilidad.
Para probar la navegación, use TestNavHostController con compose-test-rule. El controlador permite establecer la ruta inicial y verificar que navigate() activó la transición esperada. Probar NavController no requiere emulador — funciona con los matchers Semantics de Compose Test.
El método navigate(route: String) es el mecanismo principal de navegación en NavController. Acepta una cadena de ruta, NavOptions opcionales y Navigator.Extras. NavOptions controlan el comportamiento de la transición: launchSingleTop (no duplicar la ruta en la pila), popUpTo (limpiar la pila hasta una ruta), restoreState (restaurar estado anterior).
NavOptions se configuran mediante sintaxis de builder: NavOptionsBuilder. Parámetros principales: popUpTo (ruta + inclusive/saveState), launchSingleTop (Boolean, true — no crear duplicados), restoreState (restaurar estado al regresar). Sin popUpTo, cada navigate() agrega una entrada a la pila, lo que provoca acumulación del back stack y comportamiento incorrecto del botón Back.
navController.navigate("profile/42") {
popUpTo("main") { saveState = true }
launchSingleTop = true
restoreState = true
}
Navigator.Extras permite pasar datos adicionales que no forman parte de la ruta: elementos compartidos para animación, banderas Intent, bundle Pac-Man. Extras se usan raramente — principalmente para integración con Accompanist Animation o Navigators personalizados. Para la mayoría de escenarios, una cadena de ruta y NavOptions son suficientes.
popBackStack() es el método para regresar a la pantalla anterior. Sin argumentos, elimina la entrada superior de la pila y devuelve true si la eliminación fue exitosa. Si la pila está vacía, el método devuelve false y la Activity se cierra (similar a super.onBackPressed()).
La versión sobrecargada popBackStack(route: String, inclusive: Boolean) elimina todas las entradas hasta la ruta especificada. Si inclusive = true, la ruta especificada también se elimina. El método devuelve Boolean — true si se encontraron y eliminaron entradas. La versión con inclusive es útil para escenarios de “salida a la pantalla raíz” después de autorización o finalización de pedido.
| Método | Descripción | Ejemplo |
|---|---|---|
| popBackStack() | Regreso una pantalla atrás | navController.popBackStack() |
| popBackStack(route, false) | Limpiar hasta route (route permanece) | popBackStack(“home”, false) |
| popBackStack(route, true) | Limpiar hasta e incluyendo route | popBackStack(“home”, true) |
| navigate(route) { popUpTo(route) { inclusive = true } } | Navegar con limpieza completa | navigate(“login”) { popUpTo(0) { inclusive = true } } |
Para manejar el botón Back del sistema (hardware back button), use BackHandler de Compose. BackHandler acepta enabled y onBack — un callback invocado al presionar. Para Android 14+, se usa PredictiveBackGesture, integrado mediante NavController desde la versión 2.9.0. Predictive back agrega una animación de vista previa del regreso.
SavedStateHandle es un mecanismo para preservar el estado de ViewModel durante la navegación y reconfiguración. NavController proporciona automáticamente SavedStateHandle para cada NavBackStackEntry. A través de SavedStateHandle, ViewModel almacena el estado de la pantalla y lo restaura al regresar (restoreState = true).
En Navigation Compose, SavedStateHandle se usa junto con ViewModel: ViewModel se inicializa mediante SavedStateHandle, que se pasa desde backStackEntry. Al navegar a otra pantalla y regresar (con restoreState), ViewModel recibe el estado guardado en lugar de crearse de nuevo. Esto es crítico para pantallas con entrada de datos, filtros o desplazamiento.
class ProfileViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String = savedStateHandle.get<String>("userId") ?: ""
var searchQuery by savedStateHandle.getStateFlow("search", "")
.collectAsState()
}
SavedStateHandle admite tipos primitivos, String, Bundle y Parcelable. Para objetos complejos, guarde solo IDs y cargue los datos completos desde el repositorio. El límite de SavedStateHandle es de aproximadamente 1 MB, superarlo causa TransactionTooLargeException. Para volúmenes grandes, use Room o DataStore en lugar de guardar en el handle.
Importante: SavedStateHandle preserva el estado solo cuando se usa restoreState = true en NavOptions. Si restoreState no se especifica, ViewModel se crea de nuevo con valores predeterminados al regresar. Para la conmutación de BottomNavigation con restoreState, NavController preserva el estado de cada pestaña y lo restaura al volver a seleccionarla.
currentBackStackEntryAsState() es una función que devuelve State<NavBackStackEntry?>, que se actualiza con cada cambio de la ruta actual. Este es el mecanismo principal para la sincronización de la UI con la navegación: BottomNavigation resalta el elemento activo, Toolbar actualiza el título, Drawer se cierra al navegar.
La función funciona mediante snapshotFlow y collectAsState: cuando el back stack cambia, Compose recompone los elementos suscritos. Importante: currentBackStackEntryAsState() se actualiza solo después de completar la animación de transición. Para actualizaciones inmediatas, use currentDestination, que cambia sincrónicamente con navigate() pero no admite estado.
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Text(
text = when (currentRoute) {
"home" -> "Home"
"profile" -> "Profile"
else -> ""
}
)
Para acceder a los argumentos de la ruta actual, use navBackStackEntry?.arguments. Esto es conveniente en BottomNavigation: selectedItem se calcula basado en currentRoute. Para depuración de navegación, use NavController.addOnDestinationChangedListener() que registra cada transición. En producción, evite suscripciones dentro de una gran cantidad de composables — cree una fuente única en ViewModel y pase State a la UI.
Preguntas Frecuentes
Técnicamente sí, pero no se recomienda. Un solo NavController garantiza un back stack consistente y simplifica la depuración. Múltiples controladores están justificados solo para grafos anidados con navegación separada (por ejemplo, modal bottom sheet con su propia pila).
Pase NavController a ViewModel mediante constructor o DI. Sin embargo, es mejor pasar solo funciones callback (onNavigate, onBack) en lugar del NavController mismo — esto simplifica las pruebas. Para eventos, use Channel<NavEvent> en ViewModel y recolecte en la UI.
El problema es el ciclo de vida: si NavController aún no está inicializado (NavHost no construido), navigate() se ignora. Use LaunchedEffect para llamar a la navegación después de cargar datos, no dentro de una corrutina con ciclo de vida arbitrario.
Llame a navController.navigate(“target”) { popUpTo(0) { inclusive = true } }. El parámetro popUpTo(0) limpia la pila por completo, inclusive = true también elimina la entrada inicial. El flag launchSingleTop = true evita rutas duplicadas.
NavHostController es una subclase de NavController con métodos adicionales para NavHost (por ejemplo, setOnBackStackChangedListener). NavController es la clase base que se puede usar fuera de NavHost para gestión programática de la pila. En la mayoría de los casos, se usa NavHostController.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también