NavController — esencia, métodos y gestión de navegación en Jetpack Compose

Autor: IT Sectr Publicado: 2026-06-29 Tiempo de lectura: 7 min

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 — el controlador central de navegación de Compose, que gestiona el back stack y las transiciones entre pantallas
  • navigate() — el método principal para navegar a una ruta con soporte de NavOptions para la gestión de la pila
  • popBackStack() — regreso a la pantalla anterior con limpieza opcional hasta una ruta específica
  • SavedStateHandle — integración con ViewModel para preservar el estado de la pantalla durante la navegación
  • currentBackStackEntryAsState() — observación de la ruta actual para sincronización de la UI

¿Qué es NavController en Jetpack Compose?

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.

kotlin
@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.

kotlin
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: gestión de regreso y limpieza de pila

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étodoDescripciónEjemplo
popBackStack()Regreso una pantalla atrásnavController.popBackStack()
popBackStack(route, false)Limpiar hasta route (route permanece)popBackStack(“home”, false)
popBackStack(route, true)Limpiar hasta e incluyendo routepopBackStack(“home”, true)
navigate(route) { popUpTo(route) { inclusive = true } }Navegar con limpieza completanavigate(“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: preservación del estado de pantalla

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.

kotlin
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.

Observación de la ruta actual mediante currentBackStackEntryAsState

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.

kotlin
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

¿Se pueden crear varios NavControllers en una sola Activity?

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).

¿Cómo pasar NavController a través de ViewModel?

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.

¿Por qué navigate no funciona después de una operación asíncrona?

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.

¿Cómo limpiar todo el back stack y navegar a una nueva pantalla?

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.

¿Cuál es la diferencia entre NavHostController y NavController?

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

  • NavController — el componente central de Navigation Compose, que gestiona la pila de rutas y las transiciones entre pantallas
  • navigate() realiza transiciones con configuración popUpTo, launchSingleTop y restoreState mediante NavOptions
  • popBackStack() gestiona el regreso: paso individual o limpieza masiva hasta una ruta específica con inclusive
  • SavedStateHandle se integra con ViewModel para preservación automática del estado de pantalla durante la navegación
  • currentBackStackEntryAsState() proporciona observación reactiva de la ruta actual para sincronización de la UI
  • BackHandler maneja el botón Back del sistema, y PredictiveBackGesture es compatible desde NavController 2.9.0
  • Para pruebas, use TestNavHostController con compose-test-rule y matchers Semantics

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.

Discutir el proyecto

Lea también