Tema 3. Navigation Compose

  • Bloque: B2 — Arquitectura MVVM y desarrollo de aplicaciones Android
  • Duración aproximada: 10 horas
  • RA2 — Desarrolla aplicaciones para dispositivos móviles analizando y empleando las tecnologías y librerías específicas.
Código Criterio
RA2-a Se ha generado la estructura de clases necesaria para la aplicación.
RA2-c Se han utilizado las clases necesarias para la conexión y comunicación con dispositivos inalámbricos.
RA2-g Se han realizado pruebas de interacción usuario-aplicación para optimizar las aplicaciones desarrolladas a partir de emuladores.
RA2-i Se han documentado los procesos necesarios para el desarrollo de las aplicaciones.

Dependencias necesarias#

Se parte de la última versión de AppDummy, si no lo tienes, puedes descargarla desde v4. Ejemplo práctico Tema 2 . Se añaden las siguientes dependencias al build.gradle.kts del módulo app:

 1// build.gradle.kts — plugins (nivel de proyecto)
 2plugins {
 3    id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin apply false
 4}
 5
 6// build.gradle.kts (app) — plugins del módulo
 7plugins {
 8    id("org.jetbrains.kotlin.plugin.serialization")
 9}
10
11// build.gradle.kts (app) — dependencias
12dependencies {
13    // Navigation Compose (API tipada disponible desde 2.8.0)
14    implementation("androidx.navigation:navigation-compose:2.9.8")
15
16    // Kotlin Serialization — imprescindible para las rutas @Serializable
17    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
18}

Compatibilidad de versiones: El plugin kotlin.plugin.serialization debe tener exactamente la misma versión que Kotlin. Si tu proyecto usa Kotlin 2.1.20, el plugin debe ser también 2.1.20. Una discrepancia producirá errores de compilación difíciles de diagnosticar. Puedes hacer uso de la variable libs.versions.kotlin si estás usando el catálogo de versiones.


1. Introducción: una sola Activity, múltiples pantallas#

En el Bloque 1 se vio el enfoque clásico de múltiples Activities: cada pantalla es una Activity, y se navega entre ellas con Intent. En el desarrollo moderno con Jetpack Compose, el patrón recomendado es la Single Activity Architecture: una única MainActivity que contiene un NavHost, y cada pantalla es un @Composable.

Enfoque clásico (Bloque 1)          Enfoque moderno (este tema)
─────────────────────────           ──────────────────────────
MainActivity                        MainActivity
ListadoActivity               →         setContent {
DetalleActivity                             NavHost {
FavoritosActivity                               Pantalla Listado
                                                Pantalla Detalle
                                                Pantalla Favoritos
                                            }
                                        }

Las ventajas del enfoque con NavHost son: navegación declarativa y gestionada automáticamente, gestión del botón Atrás sin código adicional, paso de argumentos tipado y seguro, animaciones de transición integradas, y compatibilidad directa con ViewModel por destino.


2. Componentes de Navigation Compose#

NavController es el objeto central de la navegación. Mantiene la pila de navegación (back stack) y proporciona métodos para navegar entre destinos. En Compose se obtiene con rememberNavController().

1// Crear el NavController — siempre en el nivel más alto posible
2// (normalmente en MainActivity o en el composable raíz)
3val navController = rememberNavController()

Nunca se debe pasar el NavController directamente a los composables de pantalla. En su lugar, se pasan lambdas de navegación (callbacks). Esto hace que las pantallas sean independientes de Navigation, lo que facilita su testeo y reutilización.

NavHost es el contenedor que muestra el contenido del destino actual según el grafo de navegación. Requiere el NavController y un destino de inicio:

 1NavHost(
 2    navController = navController,
 3    startDestination = Listado   // objeto @Serializable, no un String
 4) {
 5    composable<Listado> {
 6        PantallaListado(onNavegaADetalle = { id -> navController.navigate(Detalle(id)) })
 7    }
 8    composable<Detalle> { backStackEntry ->
 9        val ruta = backStackEntry.toRoute<Detalle>()
10        PantallaDetalle(libroId = ruta.id, onVolver = { navController.navigateUp() })
11    }
12}

Back Stack (pila de navegación)#

Cada llamada a navController.navigate(destino) apila el nuevo destino encima del actual. El botón Atrás del dispositivo desapila el destino superior automáticamente:

Inicio:   [Listado]
navigate(Detalle(1)):  [Listado] → [Detalle(1)]
navigate(Favoritos):   [Listado] → [Detalle(1)] → [Favoritos]
navigateUp():          [Listado] → [Detalle(1)]
navigateUp():          [Listado]

3. Rutas tipadas con @Serializable#

3.1 La evolución de las rutas en Navigation Compose#

Antes de Navigation 2.8.0, las rutas eran cadenas de texto con argumentos embebidos como fragmentos de URL:

1// ❌ API antigua (antes de Navigation 2.8) — evitar en proyectos nuevos
2composable("detalle/{id}") { backStackEntry ->
3    val id = backStackEntry.arguments?.getString("id")?.toInt() ?: 0
4    PantallaDetalle(libroId = id)
5}
6navController.navigate("detalle/42")

Este enfoque presentaba varios problemas: los errores tipográficos en los strings producían crashes en tiempo de ejecución, el compilador no verificaba que los argumentos fueran correctos, y el código de extracción de argumentos era verbose y propenso a errores.

A partir de Navigation 2.8.0, las rutas se definen como clases y objetos de Kotlin anotados con @Serializable:

1// ✅ API tipada (Navigation 2.8+) — recomendada
2@Serializable
3data object Listado   // sin argumentos → data object
4
5@Serializable
6data class Detalle(val id: Int)   // con argumentos → data class
7
8navController.navigate(Detalle(id = 42))   // tipado, seguro en compilación

3.2 Reglas para definir rutas#

 1import kotlinx.serialization.Serializable
 2
 3// Sin argumentos → data object
 4@Serializable
 5data object Listado
 6
 7@Serializable
 8data object Favoritos
 9
10// Con argumento requerido → data class
11@Serializable
12data class Detalle(val id: Int)
13
14// Con argumento opcional (nullable) → tipo nullable con valor por defecto
15@Serializable
16data class Busqueda(val query: String? = null)
17
18// Con varios argumentos y valores por defecto
19@Serializable
20data class Edicion(
21    val id: Int,
22    val esNueva: Boolean = false
23)

Los tipos de argumento soportados nativamente son Int, Long, Float, Boolean, String y List<Int>. Para tipos personalizados (enums, data classes complejas) es necesario proporcionar un NavType personalizado, lo cual queda fuera del alcance de este tema.

¿Dónde definir las rutas? Se recomienda crear un archivo dedicado navegacion/Rutas.kt con todas las definiciones @Serializable. Así, cualquier parte del código puede importarlas sin crear dependencias circulares.

3.3 Extraer argumentos con toRoute<T>()#

Dentro de un bloque composable<T>, se accede a los argumentos de la ruta actual con backStackEntry.toRoute<T>():

1composable<Detalle> { backStackEntry ->
2    // toRoute<T>() deserializa la ruta y devuelve el objeto con los argumentos
3    val ruta: Detalle = backStackEntry.toRoute<Detalle>()
4
5    PantallaDetalle(
6        libroId = ruta.id,
7        onVolver = { navController.navigateUp() }
8    )
9}

Desde un ViewModel, los mismos argumentos son accesibles a través de SavedStateHandle:

1class DetalleViewModel(
2    savedStateHandle: SavedStateHandle
3) : ViewModel() {
4    // toRoute<T>() también está disponible en SavedStateHandle
5    private val ruta: Detalle = savedStateHandle.toRoute<Detalle>()
6    val libroId: Int = ruta.id
7}

4. Callbacks de navegación en lugar del NavController#

La documentación oficial de Android recomienda explícitamente no pasar el NavController directamente a los composables de pantalla. En su lugar, cada pantalla recibe lambdas de navegación como parámetros:

 1// ✅ Patrón recomendado — la pantalla no sabe nada de Navigation
 2// ─── screens/detalle/PantallaDetalle.kt ──────────────────────────────────────────────────────────
 3@OptIn(ExperimentalMaterial3Api::class)
 4@Composable
 5fun PantallaDetalle(
 6    libroId: Int,
 7    onVolver: () -> Unit = {},
 8    onCompartir: (String) -> Unit = {},
 9    viewModel: DetalleViewModel = viewModel(factory = DetalleViewModel.Factory)
10) {
11    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
12
13    Scaffold(
14        topBar = {
15            TopAppBar(
16                title = { Text("Detalle") },
17                navigationIcon = {
18                    IconButton(onClick = onVolver) {
19                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver")
20                    }
21                }
22            )
23        }
24    ) { padding ->
25        // contenido de la pantalla...
26    }
27}
1// ❌ Antipatrón — la pantalla está acoplada a Navigation
2@Composable
3fun PantallaDetalleMal(
4    libroId: Int,
5    navController: NavController   // difícil de testear, acoplado a Navigation
6) {
7    // ...
8    Button(onClick = { navController.navigateUp() }) { Text("Volver") }
9}

Las ventajas de los callbacks son que las pantallas se pueden testear sin instanciar un NavController, se pueden usar en previews de Android Studio sin configuración adicional, y las pantallas son reutilizables independientemente del grafo de navegación.


5. Grafo de navegación de AppDummy#

5.1 Definición de rutas#

1import kotlinx.serialization.Serializable
2
3// ─── navegacion/Rutas.kt ─────────────────────────────────────────────────────────────────────────
4
5@Serializable data object Inicio
6@Serializable data object Listado
7@Serializable data class Detalle(val id: Int)
8@Serializable data object Favoritos

5.2 NavHost completo de AppDummy#

 1// ─── navegacion/AppNavigation.kt ─────────────────────────────────────────────────────────────────
 2@Composable
 3fun AppNavigation(navController: NavHostController = rememberNavController()) {
 4    Scaffold(
 5        bottomBar = {
 6            AppDummyBottomBar(navController = navController)
 7        }
 8    ) { innerPadding ->
 9        NavHost(
10            navController = navController,
11            startDestination = Listado,
12            modifier = Modifier.padding(innerPadding)
13        ) {
14
15            composable<Listado> {
16                PantallaListado(    // <- Composable de la pantalla listado.
17                    onNavegaADetalle = { id ->
18                        navController.navigate(Detalle(id = id))    // <- Ruta de Navegación a la pantalla detalle con el id del libro.
19                    }
20                )
21            }
22
23            composable<Detalle> { backStackEntry ->
24                val ruta: Detalle = backStackEntry.toRoute<Detalle>()
25                PantallaDetalle(
26                    libroId = ruta.id,
27                    onVolver = { navController.navigateUp() }
28                )
29            }
30        }
31    }
32}

5.3 Inicialización en MainActivity#

 1// MainActivity.kt
 2class MainActivity : ComponentActivity() {
 3    override fun onCreate(savedInstanceState: Bundle?) {
 4        super.onCreate(savedInstanceState)
 5        setContent {
 6            AppDummyTheme {
 7                AppNavigation()
 8            }
 9        }
10    }
11}

6. Navegación con BottomNavigationBar#

La barra de navegación inferior permite al usuario cambiar entre las secciones principales de la app. Se integra con el NavController mediante currentBackStackEntryAsState(), que devuelve el destino actual de forma reactiva.

6.1 Definición de ítems del BottomBar#

 1import androidx.compose.material.icons.Icons
 2import androidx.compose.material.icons.filled.*
 3import androidx.compose.ui.graphics.vector.ImageVector
 4
 5// ─── navegacion/ItemsNavegacion.kt ───────────────────────────────────────────────────────────────
 6
 7data class ItemNavegacion<T : Any>(
 8    val etiqueta: String,
 9    val ruta: T,
10    val icono: ImageVector,
11    val iconoSeleccionado: ImageVector = icono
12)
13
14val itemsNavegacionPrincipal = listOf(
15    ItemNavegacion(
16        etiqueta = "Catálogo",
17        ruta = Listado, // <- Ruta de Navegación a la pantalla listado.
18        icono = Icons.Outlined.Book,
19        iconoSeleccionado = Icons.Filled.Book
20    ),
21    ItemNavegacion(
22        etiqueta = "Favoritos",
23        ruta = Favoritos,
24        icono = Icons.Default.FavoriteBorder,
25        iconoSeleccionado = Icons.Filled.Favorite
26    )
27)

6.2 Composable del BottomBar#

 1import androidx.compose.material3.*
 2import androidx.compose.runtime.*
 3import androidx.navigation.NavController
 4import androidx.navigation.NavDestination.Companion.hasRoute
 5import androidx.navigation.NavDestination.Companion.hierarchy
 6import androidx.navigation.NavGraph.Companion.findStartDestination
 7import androidx.navigation.compose.currentBackStackEntryAsState
 8
 9// ─── navegacion/AppDummyBottomBar.kt ─────────────────────────────────────────────────────────────
10@Composable
11fun AppDummyBottomBar(navController: NavController) {
12    // currentBackStackEntryAsState: State reactivo del destino actual
13    // Cuando el usuario navega, este State cambia y la barra se recompone
14    val navBackStackEntry by navController.currentBackStackEntryAsState()
15    val destinoActual = navBackStackEntry?.destination
16
17    // Solo mostrar el BottomBar en las pantallas principales
18    // (no en PantallaDetalle, por ejemplo)
19    val mostrarBottomBar = itemsNavegacionPrincipal.any { item ->
20        destinoActual?.hierarchy?.any { it.hasRoute(item.ruta::class) } == true
21    }
22
23    if (mostrarBottomBar) {
24        NavigationBar {
25            itemsNavegacionPrincipal.forEach { item ->
26                // hierarchy: recorre la cadena de destinos desde el actual hasta la raíz
27                // Útil con grafos anidados para resaltar el ítem correcto
28                val seleccionado = destinoActual?.hierarchy?.any {
29                    it.hasRoute(item.ruta::class)
30                } == true
31
32                NavigationBarItem(
33                    selected = seleccionado,
34                    onClick = {
35                        navController.navigate(item.ruta) {
36                            // popUpTo: vuelve al inicio antes de navegar
37                            // evita acumular destinos en la pila al cambiar de pestaña
38                            popUpTo(navController.graph.findStartDestination().id) {
39                                saveState = true   // guarda el estado de la pantalla anterior
40                            }
41                            // launchSingleTop: no apila múltiples copias del mismo destino
42                            launchSingleTop = true
43                            // restoreState: restaura el estado guardado con saveState
44                            restoreState = true
45                        }
46                    },
47                    icon = {
48                        Icon(
49                            imageVector = if (seleccionado) item.iconoSeleccionado else item.icono,
50                            contentDescription = item.etiqueta
51                        )
52                    },
53                    label = { Text(item.etiqueta) }
54                )
55            }
56        }
57    }
58}

popUpTo, launchSingleTop y restoreState: Estos tres parámetros del bloque navigate { } trabajan juntos para implementar el comportamiento estándar de un BottomBar. Sin ellos, cada toque en un ítem apilaría un nuevo destino, haciendo que el botón Atrás tuviera que recorrer toda la pila antes de salir de la app.


7. Gestión de la pila de navegación#

7.1 navigateUp() vs popBackStack()#

Ambas funciones navegan hacia atrás, pero con diferencias importantes:

navController.navigateUp() navController.popBackStack()
Comportamiento si la pila está vacía No hace nada (seguro) La pantalla puede quedar vacía
Con deep links Navega a la app anterior Solo desapila destinos propios
Uso recomendado Botón Atrás en TopAppBar Lógica de navegación programática
1// ✅ Para el botón Atrás en la barra superior → navigateUp()
2IconButton(onClick = { navController.navigateUp() }) {
3    Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver")
4}
5
6// ✅ Para volver al listado desde cualquier punto → popBackStack tipado
7navController.popBackStack<Listado>(inclusive = false)

7.2 Opciones de navigate { }#

El bloque lambda de navigate { } permite configurar cómo se modifica la pila al navegar:

1navController.navigate(Listado) {
2    // Eliminar todo hasta PantallaListado antes de navegar
3    popUpTo<Listado> {
4        inclusive = true    // true: también elimina PantallaListado
5        saveState = true    // guarda el estado para poder restaurarlo
6    }
7    launchSingleTop = true  // no crear una nueva instancia si ya está en la cima
8    restoreState = true     // restaurar estado guardado previamente con saveState
9}

8. ViewModel por destino y ViewModel compartido#

8.1 ViewModel por destino#

Cuando se llama a viewModel() dentro de un bloque composable<T>, el ViewModel se crea con el scope del destino de navegación. Se destruye automáticamente al salir del destino (al hacer navigateUp() o popBackStack()):

 1NavHost(...) {
 2    composable<Detalle> { backStackEntry ->
 3        val ruta = backStackEntry.toRoute<Detalle>()
 4
 5        // Este ViewModel se destruye al salir de PantallaDetalle
 6        val viewModel: DetalleViewModel = viewModel(
 7            factory = DetalleViewModel.factoryConId(ruta.id)
 8        )
 9        PantallaDetalle(viewModel = viewModel, onVolver = { navController.navigateUp() })
10    }
11}

8.2 ViewModel compartido entre destinos#

Para compartir un ViewModel entre varias pantallas (por ejemplo, un flujo de formulario de varios pasos), se alcanza el scope del grafo padre con navController.getBackStackEntry<T>(). El ViewModel se destruye cuando el grafo padre sale de la pila:

 1// Rutas del flujo de edición
 2@Serializable data object GrafoPedido      // identificador del grafo anidado
 3@Serializable data object PasoSeleccion
 4@Serializable data object PasoConfirmacion
 5
 6NavHost(...) {
 7    // Grafo anidado — las pantallas dentro comparten el mismo ViewModel
 8    navigation<GrafoPedido>(startDestination = PasoSeleccion) {
 9
10        composable<PasoSeleccion> { backStackEntry ->
11            val entradaPadre = remember(backStackEntry) {
12                navController.getBackStackEntry<GrafoPedido>()
13            }
14            // El Factory puede ser el mismo para ambas pantallas
15            val viewModelCompartido: PedidoViewModel = viewModel(
16                viewModelStoreOwner = entradaPadre,
17                factory = PedidoViewModel.Factory
18            )
19            PantallaPasoSeleccion(
20                viewModel = viewModelCompartido,
21                onSiguiente = { navController.navigate(PasoConfirmacion) }
22            )
23        }
24
25        composable<PasoConfirmacion> { backStackEntry ->
26            val entradaPadre = remember(backStackEntry) {
27                navController.getBackStackEntry<GrafoPedido>()
28            }
29            val viewModelCompartido: PedidoViewModel = viewModel(
30                viewModelStoreOwner = entradaPadre,
31                factory = PedidoViewModel.Factory
32            )
33            PantallaPasoConfirmacion(
34                viewModel = viewModelCompartido,
35                onConfirmar = { navController.popBackStack<GrafoPedido>(inclusive = true) }
36            )
37        }
38    }
39}

El remember(backStackEntry) es esencial para evitar llamar a getBackStackEntry() en cada recomposición, lo que produciría comportamientos inesperados.


Desarrollo práctico guiado: AppDummy con navegación integrada 💻#

A continuación, se muestra la integración completa de AppDummy con el sistema de navegación tipada. Este código integra todo lo desarrollado en T2 (ViewModel + StateFlow) con la navegación de este tema, Bloque 2 completo.

 1// ─── datal/model/Libro.kt ────────────────────────────────────────────────────────────────────────
 2data class Libro(
 3    val id: Int = 0,
 4    val titulo: String = "",
 5    val autor: String = "",
 6    val year: Int? = 1900,
 7    val isbn: String = "",
 8    val cover: String = "",
 9    val esFavorito: Boolean = false,
10    val leido: Boolean = false
11)
1// ─── navegacion/Rutas.kt ─────────────────────────────────────────────────────────────────────────
2import kotlinx.serialization.Serializable
3
4@Serializable data object Inicio
5@Serializable data object Listado
6@Serializable data class Detalle(val id: Int)
7@Serializable data object Favoritos
1// ─── screens/detalle/DetalleUiState.kt ───────────────────────────────────────────────────────────
2sealed class DetalleUiState {
3    data object Cargando : DetalleUiState()
4    data class Exito(val libro: Libro) : DetalleUiState()
5    data object NoEncontrado : DetalleUiState()
6}
 1// ─── screens/detalle/DetalleViewModel.kt ─────────────────────────────────────────────────────────
 2class DetalleViewModel(
 3    private val libroId: Int,
 4    private val repository: LibrosRepository
 5) : ViewModel() {
 6
 7    private val _uiState = MutableStateFlow<DetalleUiState>(DetalleUiState.Cargando)
 8    val uiState: StateFlow<DetalleUiState> = _uiState.asStateFlow()
 9
10    init {
11        cargarDetalle()
12    }
13
14    private fun cargarDetalle() {
15        viewModelScope.launch {
16            val libro = repository.getLibroPorId(libroId)
17            _uiState.value = if (libro != null) DetalleUiState.Exito(libro)
18            else DetalleUiState.NoEncontrado
19        }
20    }
21
22    companion object {
23        fun factoryConId(id: Int): ViewModelProvider.Factory = viewModelFactory {
24            initializer {
25                DetalleViewModel(
26                    libroId = id,
27                    repository = LibrosRepository()
28                )
29            }
30        }
31    }
32}
 1// ─── screens/detalle/PantallaDetalle.kt ──────────────────────────────────────────────────────────
 2@OptIn(ExperimentalMaterial3Api::class)
 3@Composable
 4fun PantallaDetalle(
 5    libroId: Int,
 6    onVolver: () -> Unit,
 7    viewModel: DetalleViewModel = viewModel(factory = DetalleViewModel.factoryConId(libroId))
 8) {
 9    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
10
11    Scaffold(
12        topBar = {
13            TopAppBar(
14                title = {
15                    val titulo = (uiState as? DetalleUiState.Exito)?.libro?.titulo ?: "Detalle"
16                    Text(titulo)
17                },
18                navigationIcon = {
19                    IconButton(onClick = onVolver) {
20                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver")
21                    }
22                }
23            )
24        }
25    ) { padding ->
26        Box(modifier = Modifier.fillMaxSize().padding(padding)) {
27            when (val estado = uiState) {
28                is DetalleUiState.Cargando ->
29                    CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
30
31                is DetalleUiState.Exito -> {
32                    Column(
33                        modifier = Modifier
34                            .fillMaxSize()
35                            .padding(24.dp),
36                        verticalArrangement = Arrangement.spacedBy(12.dp)
37                    ) {
38                        Text(
39                            text = estado.libro.titulo,
40                            style = MaterialTheme.typography.headlineMedium
41                        )
42                        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
43                            AssistChip(
44                                onClick = {},
45                                label = { Text(estado.libro.autor) }
46                            )
47                            AssistChip(
48                                onClick = {},
49                                label = { Text("${estado.libro.year}") }
50                            )
51                        }
52                        HorizontalDivider()
53                        Text(
54                            text = "Sinopsis del libro.",
55                            style = MaterialTheme.typography.bodyLarge
56                        )
57                    }
58                }
59
60                is DetalleUiState.NoEncontrado -> {
61                    Column(
62                        modifier = Modifier.fillMaxSize().padding(32.dp),
63                        horizontalAlignment = Alignment.CenterHorizontally,
64                        verticalArrangement = Arrangement.Center
65                    ) {
66                        Icon(
67                            Icons.Default.SearchOff,
68                            contentDescription = null,
69                            modifier = Modifier.size(64.dp),
70                            tint = MaterialTheme.colorScheme.onSurfaceVariant
71                        )
72                        Spacer(modifier = Modifier.height(16.dp))
73                        Text("Libro no encontrado")
74                        Spacer(modifier = Modifier.height(16.dp))
75                        Button(onClick = onVolver) { Text("Volver al catálogo") }
76                    }
77                }
78            }
79        }
80    }
81}
 1// ─── navegacion/AppNavigation.kt ─────────────────────────────────────────────────────────────────
 2@Composable
 3fun AppNavigation(
 4    navController: NavHostController = rememberNavController()
 5) {
 6    Scaffold(
 7        bottomBar = { AppDummyBottomBar(navController = navController) }
 8    ) { innerPadding ->
 9        NavHost(
10            navController = navController,
11            startDestination = Listado,
12            modifier = Modifier.padding(innerPadding)
13        ) {
14
15            composable<Listado> {
16                PantallaListado(    // <- Composable de la pantalla listado.
17                    onNavegaADetalle = { id ->
18                        navController.navigate(Detalle(id = id))    // <- Ruta de Navegación a la pantalla detalle con el id del libro.
19                    }
20                )
21            }
22
23            composable<Detalle> { backStackEntry ->
24                val ruta: Detalle = backStackEntry.toRoute<Detalle>()
25                PantallaDetalle(
26                    libroId = ruta.id,
27                    onVolver = { navController.navigateUp() }
28                )
29            }
30
31            composable<Favoritos> {
32                PantallaFavoritos(
33                    onNavegaADetalle = { id ->
34                        navController.navigate(Detalle(id = id))
35                    }
36                )
37            }
38        }
39    }
40}

Diagrama de navegación de AppDummy al final del Bloque 2#

[PantallaListado] ──navigate(Detalle(id))──► [PantallaDetalle]
       ▲                                                      │
       │◄──────────────────navigateUp()───────────────────────┘
BottomBar ──── navigate(Favoritos) ──► [PantallaFavoritos]
                    ◄──navigate(Detalle(id))──────────┘

Desarrollo práctico guiado Bloque 2#

Llegados a este punto, has completado los fundamentos del Bloque 2. El proyecto vertebrador AppDummy debe tener en este momento:

  • Una sola MainActivity con un NavHost y un BottomNavigationBar.
  • Tres pantallas principales: PantallaListado y PantallaDetalle.
  • Navegación tipada con @Serializable y toRoute<T>().
  • ViewModel por destino y gestión de la pila de navegación con navigateUp() y popBackStack().
  • Debes implementar PantallaFavoritos para que la navegación sea completa.

Referencias#

Calendar  Última modificación: jueves, 23 de julio de 2026