Setup
dependencies {
implementation("androidx.navigation:navigation-compose:2.8.4")
// Si usás Hilt:
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}
NavHost — el mapa de la app
En Compose, el NavGraph se define en código Kotlin en lugar de XML. NavHost es el composable que gestiona qué pantalla se muestra:
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "productos"
) {
composable("productos") {
ProductosScreen(
onNavADetalle = { id -> navController.navigate("detalle/$id") }
)
}
composable(
route = "detalle/{productoId}",
arguments = listOf(navArgument("productoId") { type = NavType.IntType })
) { backStackEntry ->
val id = backStackEntry.arguments?.getInt("productoId") ?: return@composable
DetalleScreen(
productoId = id,
onNavAtras = { navController.navigateUp() }
)
}
composable("perfil") {
PerfilScreen()
}
}
}
Navegar desde los ViewModels — el patrón correcto
El NavController no debe pasarse al ViewModel (rompería el principio de que el ViewModel no conoce la UI). La navegación se maneja mediante eventos del ViewModel que el Screen composable observa:
// En el ViewModel — emite un evento
fun onProductoGuardado() {
viewModelScope.launch {
repo.guardar(uiState.value.producto)
_eventos.send(ProductosEvento.NavAtras)
}
}
// En el Screen composable — observa el evento y navega
LaunchedEffect(Unit) {
viewModel.eventos.collect { evento ->
when (evento) {
is ProductosEvento.NavADetalle ->
navController.navigate("detalle/${evento.id}")
ProductosEvento.NavAtras ->
navController.navigateUp()
}
}
}
Type-safe Navigation — Navigation 2.8+
Navigation 2.8 introduce type-safe routes con @Serializable. Elimina los strings de rutas y los argumentos tipados de forma manual:
// Definí tus rutas como objetos o data classes serializables
@Serializable object Productos
@Serializable data class Detalle(val productoId: Int)
@Serializable object Perfil
// NavHost con type-safe routes
NavHost(navController, startDestination = Productos) {
composable<Productos> {
ProductosScreen(
onNavADetalle = { id -> navController.navigate(Detalle(id)) }
)
}
composable<Detalle> { backStackEntry ->
val args: Detalle = backStackEntry.toRoute()
DetalleScreen(
productoId = args.productoId,
onNavAtras = navController::navigateUp
)
}
composable<Perfil> { PerfilScreen() }
}
// Navegar — type-safe, sin strings
navController.navigate(Detalle(productoId = 42))
navController.navigate(Perfil)
Preferí type-safe navigationSi tu proyecto usa Navigation 2.8+, usá siempre las rutas tipadas. Elimina toda una clase de bugs (typos en strings, tipos incorrectos) y hace el código más legible.
Bottom Navigation con Compose
// Definir los tabs
sealed class TabDestino(val ruta: String, val icono: ImageVector, val label: String) {
object Productos : TabDestino("productos", Icons.Default.ShoppingCart, "Productos")
object Buscar : TabDestino("buscar", Icons.Default.Search, "Buscar")
object Perfil : TabDestino("perfil", Icons.Default.Person, "Perfil")
}
val tabs = listOf(TabDestino.Productos, TabDestino.Buscar, TabDestino.Perfil)
@Composable
fun AppConBottomNav() {
val navController = rememberNavController()
val navBackStack by navController.currentBackStackEntryAsState()
val rutaActual = navBackStack?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
tabs.forEach { tab ->
NavigationBarItem(
selected = rutaActual == tab.ruta,
onClick = {
navController.navigate(tab.ruta) {
// Evitar múltiples copias del mismo destino en el back stack
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(tab.icono, contentDescription = tab.label) },
label = { Text(tab.label) }
)
}
}
}
) { padding ->
NavHost(
navController = navController,
startDestination = TabDestino.Productos.ruta,
modifier = Modifier.padding(padding)
) {
composable(TabDestino.Productos.ruta) { ProductosScreen(...) }
composable(TabDestino.Buscar.ruta) { BuscarScreen(...) }
composable(TabDestino.Perfil.ruta) { PerfilScreen(...) }
}
}
}
Grafos anidados — organizar la navegación
NavHost(navController, startDestination = "auth") {
// Grafo de autenticación
navigation(startDestination = "login", route = "auth") {
composable("login") { LoginScreen(...) }
composable("registro") { RegistroScreen(...) }
}
// Grafo principal (post-login)
navigation(startDestination = "home", route = "main") {
composable("home") { HomeScreen(...) }
composable("detalle/{id}") { DetalleScreen(...) }
composable("perfil") { PerfilScreen(...) }
}
}
// Navegar y limpiar el back stack de auth:
navController.navigate("main") {
popUpTo("auth") { inclusive = true }
}
Resultado entre pantallas — SavedStateHandle
// En Compose + Hilt, el resultado de una pantalla a otra
// se pasa via SavedStateHandle del ViewModel de la pantalla ANTERIOR
// En DetalleViewModel — lee el resultado cuando vuelve
@HiltViewModel
class DetalleViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
// Navigation pone los argumentos aquí automáticamente
val productoId: Int = checkNotNull(savedStateHandle["productoId"])
}
// Para resultados de retorno (como startActivityForResult):
// La pantalla B guarda el resultado en su back stack entry
// La pantalla A lo lee cuando vuelve al foco