Saltar al contenido
K
Jetpack y arquitecturaPaso a paso38 min de lectura · 15 min de máquina Básico

Navegación en Marcador

Al terminar: Navegar en Marcador entre la lista, el partido y los goleadores con Navigation 3 (el partido abre aún siempre el 1).

Al terminar esta lección Marcador tendrá tres pantallas encadenadas: la lista de partidos, el partido que tocas y los goleadores del torneo. Al pulsar «atrás» volverás por el mismo camino, cada pantalla tendrá su propio ViewModel, y si el sistema mata el proceso, la app reabrirá en la pantalla donde estabas. Una cosa queda a medias a propósito: el partido que se abre será siempre el 1, porque el ViewModel todavía no recibe el número del partido. De eso se ocupa la lección siguiente, El id del partido: inyección asistida.

Importa porque en cuanto una app tiene más de una pantalla, la navegación decide qué se conserva y qué se pierde al ir y volver. Es la segunda mitad de Navigation 3: la pila de pantallas: aquella dejó la librería instalada y el museo funcionando; esta la aplica a Marcador. Temas: aplicar claves, pila y decoradores a una app real, y una pantalla nueva con su ViewModel.

Ejercicio · una pila de pantallas de juguete

Antes de tocar Marcador, practica las dos funciones que mueven la pila (volver e irAPestana) con Kotlin puro. Usa tres piezas de Kotlin que conviene tener claras:

  • Una sealed interface declara un conjunto cerrado de tipos: solo valen los que declares tú, y por eso todas las pantallas pueden ser «una Clave». Cada pantalla sin datos es un data object y cada una con datos, una data class.
  • mutableListOf<Clave>(Lista) es una lista que se puede modificar, aquí con Lista dentro desde el principio.
  • pila.lastOrNull() es el último elemento, o null si la lista está vacía.
ejemplo mínimo: una pila de juguete (con comentarios)
sealed interface Pantalla                                 // solo habrá las pantallas que declare yo
data object Inicio : Pantalla                             // sin datos
data class Detalle(val id: Int) : Pantalla                // con datos: imprime Detalle(id=7)
 
fun main() {
    val pila = mutableListOf<Pantalla>(Inicio)            // lista modificable, empieza con Inicio
    pila.add(Detalle(7))                                  // navegar = añadir al final
    println(pila.lastOrNull())                            // Detalle(id=7): la de arriba
    pila.removeAt(pila.lastIndex)                         // volver = quitar la última
    println(pila)                                         // [Inicio]
}

Reto · Ejercicio: una pila de pantallas de juguete

En el Kotlin Playground, fuera de tu proyecto.

  • Pantallas: una lista, unas tablas, unos goleadores y un partido que lleva su número. Al imprimir una pila, las pantallas sin datos salen con su nombre y el partido con su número, como en la salida de abajo.
  • La pila: una lista que se puede modificar y que empieza en la lista.
  • volver: quita la última pantalla solo si queda más de una, y dice si lo hizo.
  • irAPestana: si el destino ya es la pantalla actual, no hace nada; si no, vacía la pila, deja la lista debajo (salvo que el destino sea la propia lista) y pone el destino encima.
  • En main: añade el partido 3 y los goleadores e imprime la pila; vuelve una vez, ve a las tablas e imprime la pila; después vuelve dos veces e imprime lo que devuelve cada vez.

La consola debe mostrar:

[Lista, Partido(id=3), Goleadores]
[Lista, Tablas]
true
false
Ver solución
solución: la pila de juguete
sealed interface Clave
data object Lista : Clave
data object Tablas : Clave
data class Partido(val id: Int) : Clave
data object Goleadores : Clave
 
fun volver(pila: MutableList<Clave>): Boolean {
    if (pila.size <= 1) return false
    pila.removeAt(pila.lastIndex)
    return true
}
 
fun irAPestana(pila: MutableList<Clave>, destino: Clave) {
    if (pila.lastOrNull() == destino) return
    pila.clear()
    if (destino != Lista) pila.add(Lista)
    pila.add(destino)
}
 
fun main() {
    val pila = mutableListOf<Clave>(Lista)
    pila.add(Partido(3))
    pila.add(Goleadores)
    println(pila)                 // [Lista, Partido(id=3), Goleadores]
    volver(pila)
    irAPestana(pila, Tablas)
    println(pila)                 // [Lista, Tablas]
    println(volver(pila))         // true  (queda [Lista])
    println(volver(pila))         // false (con una sola no se vuelve)
}

La última llamada devuelve false porque quedar sin pantallas dejaría la app en blanco: por eso el onBack de la pantalla de navegación (AppMarcador, que escribes en el paso 2) comprueba pila.size > 1.

Paso · Lista, partido y goleadores

Objetivo. Sustituir las cuatro pestañas por una pila de Navigation 3. La app abre en la lista; tocar un partido lo abre en su propia pantalla; «Ver goleadores» apila la tercera; «atrás» desanda. La pestaña «Marcador» desaparece: el partido ya no es un lugar fijo de la barra, es una pantalla de detalle a la que se llega desde la lista. La barra inferior queda con tres pestañas: Partidos, Tablas y Recomp.

Lo que enseña navegación es AppMarcador.kt: ese lo escribes entero. Los otros cinco ficheros solo se adaptan a él (hay una pantalla de goleadores y el partido ya no elige partido): tres los copias con el botón Copiar fichero, y los cambios de PantallaMarcador y de PartidoViewModel los escribes tú.

app → kotlin+java → com.sebastiannarvaez.marcador
ui
AppMarcador.ktse modifica (lo escribes)
feature
marcador
ui
MarcadorUiState.ktse modifica (se copia)
PantallaMarcador.ktse modifica (los cambios, a mano)
PartidoViewModel.ktse modifica (una línea, a mano)
goleadoresfeature nueva
ui
GoleadoresViewModel.ktse crea (se copia)
PantallaGoleadores.ktse crea (se copia)
Seis ficheros: tres los escribes (AppMarcador entero y los cambios de PantallaMarcador y de PartidoViewModel) y tres los copias.

Paso 1: copia tres ficheros de apoyo y adapta PantallaMarcador y PartidoViewModel

Para cada uno de los tres, el mismo procedimiento. El que ya existe (MarcadorUiState) lo abres con doble clic en app → kotlin+java → com.sebastiannarvaez.marcador → feature → marcador → ui. Los dos nuevos son de una feature nueva: clic derecho sobre com.sebastiannarvaez.marcador → New → Package; el campo ya trae com.sebastiannarvaez.marcador: escribe .feature.goleadores.ui al final (con el punto delante). Es otra feature porque goleadores es una pantalla con nombre propio; solo tiene ui porque sus datos son los del repositorio de partidos, que ya existe. Después, clic derecho sobre ese paquete ui (en una sola línea, goleadores.ui, porque goleadores solo tiene esa hija: es la misma carpeta) → New → Kotlin Class/File → File → el nombre (GoleadoresViewModel, PantallaGoleadores). Con el fichero abierto: pulsa su botón de abajo (aparece un aviso de que se ha copiado), haz clic dentro del editor, selecciona todo con Cmd+A y pega con Cmd+V. El contenido pegado trae su propia línea package, así que sustituye lo que hubiera.

GoleadoresViewModel.kt (nuevo):

GoleadoresViewModel.kt

MarcadorUiState.kt:

MarcadorUiState.kt

PantallaGoleadores.kt (nuevo):

PantallaGoleadores.kt

Qué cambia en cada uno, para que no sea una caja negra:

  • MarcadorUiState pierde el evento Elegir: elegir un partido ya no es un evento del ViewModel, es navegar. Por eso PartidoViewModel pierde también la rama que lo atendía (un cambio de una línea, abajo).
  • GoleadoresViewModel y PantallaGoleadores son nuevos. El ViewModel es corto: pide los goleadores al repositorio y los expone como StateFlow.
GoleadoresViewModel.kt (con comentarios)
@HiltViewModel                                                  // Hilt lo construye con el repositorio, que ya sabe dar
class GoleadoresViewModel @Inject constructor(repositorio: PartidosRepository) : ViewModel() {
    // La consulta GROUP BY de Room ya cuenta los goles por jugador; el ViewModel solo los expone.
    // stateIn convierte el Flow en StateFlow: arranca al haber alguien mirando y aguanta 5 s sin
    // observadores (una rotación), y emptyList() es el valor mientras llega el primer dato.
    val goleadores: StateFlow<List<Goleador>> = repositorio.observarGoleadores()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
}

PantallaGoleadores(goleadores, alVolver) es una pantalla sin estado propio: una barra superior «Goleadores», una LazyColumn con una fila por jugador («Nico», «11 goles») y un botón «Volver al partido» que llama a alVolver.

Y dos ficheros que no se copian, sino que cambias tú. Primero PartidoViewModel.kt, una sola línea: como Elegir ya no existe, borra la rama que lo atendía.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
// Borra esta línea (está dentro de alEvento, en el when, entre el caso de Gol y el de CambiarDuracion):
        is MarcadorEvento.Elegir -> cargar(evento.partidoId)

Y PantallaMarcador.kt: la pantalla avisa y quien decide navegar es AppMarcador, así que recibe tres acciones nuevas (onVerGoleadores, onVolver, onProvocarError) y no pide el ViewModel ella misma. Son ocho cambios, en este orden:

  1. Borra el import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel: la pantalla deja de pedir su ViewModel ella misma.
  2. En PantallaMarcador, añade las tres acciones tras onVerDemos, y quita el valor por defecto de viewModel, que era hiltViewModel() (ahora se lo da AppMarcador, con el id del partido).
  3. Pasa las tres acciones a PantallaMarcadorContenido en la llamada del final.
  4. Añade las mismas tres acciones a los parámetros de PantallaMarcadorContenido.
  5. En la rama Error, el botón pasa a ser «Volver» y avisa hacia arriba.
  6. En el texto del partido y justo debajo, el botón de ver los goleadores.
  7. El botón «Provocar error (partido 99)» avisa hacia arriba en lugar de pedirle otro partido al ViewModel.
  8. La vista previa pasa las tres acciones vacías.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 1. Borra esta línea de los import:
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 2. La cabecera de PantallaMarcador queda así (sustituye la que tenías)
fun PantallaMarcador(
    onVerDemos: () -> Unit,
    onVerGoleadores: () -> Unit,
    onVolver: () -> Unit,
    onProvocarError: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: PartidoViewModel,
) {
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 3. La última línea de PantallaMarcador (la llamada a PantallaMarcadorContenido)
    PantallaMarcadorContenido(estado, pausas, viewModel::alEvento, onVerDemos, onVerGoleadores, onVolver, onProvocarError, modifier)
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 4. Los parámetros de PantallaMarcadorContenido: tres nuevos, después de onVerDemos
    onVerDemos: () -> Unit,
    onVerGoleadores: () -> Unit,
    onVolver: () -> Unit,
    onProvocarError: () -> Unit,
    modifier: Modifier = Modifier,
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 5. En la rama Error, sustituye la línea del botón «Volver al partido 1»
                Button(onClick = onVolver) { Text("Volver") }
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 6. En extras = { … }: sustituye la línea que empieza por Text("Partido n.º (el texto del partido) por estas dos; la segunda es el botón nuevo
                        Text("Partido n.º ${estado.partidoId} (elegido en la lista de partidos)")
                        Button(onClick = onVerGoleadores) { Text("Ver goleadores") }
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 7. Sustituye la línea del botón «Provocar error (partido 99)», la que llamaba a onEvento(MarcadorEvento.Elegir(99))
                        Button(onClick = onProvocarError) { Text("Provocar error (partido 99)") }
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
// 8. En PantallaMarcadorPreview, dentro de la llamada a PantallaMarcadorContenido, sustituye la línea que empieza por pausas = 0 por estas dos
            pausas = 0, onEvento = {}, onVerDemos = {}, onVerGoleadores = {},
            onVolver = {}, onProvocarError = {},

Hasta que termines AppMarcador.kt el proyecto no compila: PantallaMarcador pide acciones y un ViewModel que el AppMarcador actual todavía no le da.

Paso 2: AppMarcador.kt

Piezas nuevas de este paso:

  • pila.lastOrNull() es la clave actual, o null si la pila está vacía (add, removeAt y clear() los usaste en el museo de Navigation 3: la pila de pantallas).
  • AnimatedVisibility(visible, enter, exit) muestra u oculta con animación; slideInVertically { it } desliza desde abajo.
  • @OptIn(ExperimentalMaterial3Api::class) es un permiso explícito para usar una pieza de Material 3 que Google todavía puede cambiar; CenterAlignedTopAppBar(title) es la barra superior con el título centrado. Sin el @OptIn el compilador se niega.
  • Un Scaffold dentro de otro: el de fuera pone la barra inferior y los de dentro su barra superior; todos con contentWindowInsets = WindowInsets(0), o sale margen doble. Los insets son el hueco que ocupan las barras del sistema (la de estado arriba, la de gestos abajo); con WindowInsets(0) el Scaffold no lo reserva, y lo reservan las barras (más detalle en Pestañas con NavigationBar).

Ya vistas: a to b y { (a, b) -> … } (Colecciones); x in lista (Clases y objetos).

Esas piezas, con los dos decoradores y un ViewModel propio por receta abierta, en un recetario inventado (otras claves, otras pantallas):

ejemplo mínimo: pestañas, decoradores y un ViewModel por entrada (con comentarios)
private val SECCIONES: List<NavKey> = listOf(RecetasKey, CompraKey)
 
@Composable
fun AppRecetario() {
    val pila = rememberNavBackStack(RecetasKey)
    val actual = pila.lastOrNull()                                   // la clave de arriba, o null si la pila está vacía
    Scaffold(
        bottomBar = {
            AnimatedVisibility(
                visible = actual in SECCIONES,                       // in: ¿está en la lista?
                enter = slideInVertically { it },                    // entra deslizándose desde abajo
                exit = slideOutVertically { it },                    // sale hacia abajo
            ) {
                NavigationBar {
                    listOf(RecetasKey to "Recetas", CompraKey to "Compra")   // to: un Pair (clave, título)
                        .forEach { (clave, titulo) ->                // desestructura el Pair en dos nombres
                            NavigationBarItem(
                                selected = actual == clave,
                                onClick = { pila.clear(); pila.add(clave) },  // vacía la pila y deja solo esa sección
                                icon = {},
                                label = { Text(titulo) },
                            )
                        }
                }
            }
        },
    ) { paddingValues ->
        NavDisplay(
            modifier = Modifier.padding(paddingValues),
            backStack = pila,
            onBack = { if (pila.size > 1) pila.removeAt(pila.lastIndex) },
            entryDecorators = listOf(
                rememberSaveableStateHolderNavEntryDecorator(),      // primero: rememberSaveable propio por entrada
                rememberViewModelStoreNavEntryDecorator(),           // después: ViewModels propios por entrada
            ),
            entryProvider = entryProvider {
                entry<RecetasKey> { ListaDeRecetas(onAbrir = { id -> pila.add(RecetaKey(id)) }) }
                entry<CompraKey> { Text("Lista de la compra") }
                entry<RecetaKey> {
                    val vm: RecetaViewModel = hiltViewModel()        // uno por receta abierta, gracias al decorador
                    PantallaReceta(vm, onVolver = { pila.removeAt(pila.lastIndex) })
                }
            },
        )
    }
}

Intenta escribirlo tú. Lo que ya existe en los ficheros que acabas de pegar, para que encaje: PantallaMarcador recibe cuatro acciones (ver las demos, ver los goleadores, volver y provocar el error) y su ViewModel; PantallaGoleadores recibe la lista de goleadores y qué hacer al volver; GoleadoresViewModel da los goleadores como un flujo de estado; y PartidoViewModel ya no atiende el evento Elegir.

Reescribe AppMarcador.kt entero (ábrelo en app → kotlin+java → com.sebastiannarvaez.marcador → ui):

  1. Cinco claves que se puedan guardar: la lista, las tablas, la recomposición, el partido con su id y los goleadores; y las tres primeras, como las pestañas.
  2. Una pila que empieza en la lista, y una barra inferior con «Partidos», «Tablas» y «Recomp.» que solo se ve (entrando y saliendo por abajo con animación) cuando la clave de arriba es una pestaña.
  3. La pantalla que dibuja la pila, con su estado guardado y sus ViewModel propios por entrada, que al ir atrás quita la de arriba si queda más de una. Una entrada por clave:
    • Partidos, Tablas y Recomp.: las tres pantallas de antes, cada una con su barra superior y su ViewModel de lista (hiltViewModel()); tocar un partido apila su clave.
    • El partido: su ViewModel (hiltViewModel()), que de momento abre siempre el partido 1; ver goleadores apila los goleadores, volver quita la de arriba y provocar el error apila el partido 99.
    • Los goleadores: con su ViewModel (hiltViewModel()), vuelven quitando la de arriba.
  4. irAPestana, que no hace nada si ya estás en esa pestaña y, si no, vacía la pila, deja la lista debajo (salvo que el destino sea la propia lista) y pone el destino encima.

Lo que debes ver: la lista, y desde ahí el partido y los goleadores.

El título de cada bloque es la ruta del fichero en tu proyecto; en la vista Android es app → kotlin+java → com.sebastiannarvaez.marcador → ui → AppMarcador.kt. Conserva la primera línea del fichero, package com.sebastiannarvaez.marcador.ui, y sustituye todo lo demás.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código

Los import (van justo debajo del package; los que empiezan por com.sebastiannarvaez.marcador son tus pantallas y ViewModels, que ahora viven cada uno en la carpeta de su feature):

app/src/main/java/com/sebastiannarvaez/marcador/ui/AppMarcador.kt
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.sebastiannarvaez.marcador.feature.goleadores.ui.GoleadoresViewModel
import com.sebastiannarvaez.marcador.feature.goleadores.ui.PantallaGoleadores
import com.sebastiannarvaez.marcador.feature.marcador.ui.PantallaMarcador
import com.sebastiannarvaez.marcador.feature.marcador.ui.PartidoViewModel
import com.sebastiannarvaez.marcador.feature.partidos.ui.ListaViewModel
import com.sebastiannarvaez.marcador.feature.partidos.ui.PantallaPartidos
import com.sebastiannarvaez.marcador.feature.partidos.ui.PantallaTablas
import com.sebastiannarvaez.marcador.feature.partidos.ui.goleadoresConGoles
import com.sebastiannarvaez.marcador.laboratorio.PantallaRecomposicion
import kotlinx.serialization.Serializable

Primera mitad: las cinco claves y la lista de pestañas.

app/src/main/java/com/sebastiannarvaez/marcador/ui/AppMarcador.kt
@Serializable
data object ListaKey : NavKey
 
@Serializable
data object TablasKey : NavKey
 
@Serializable
data object RecomposicionKey : NavKey
 
@Serializable
data class PartidoKey(val partidoId: Int) : NavKey
 
@Serializable
data object GoleadoresKey : NavKey
 
private val PESTANAS: List<NavKey> = listOf(ListaKey, TablasKey, RecomposicionKey)

Segunda mitad: la función completa, con las cinco entradas, y irAPestana.

app/src/main/java/com/sebastiannarvaez/marcador/ui/AppMarcador.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppMarcador(onVerDemos: () -> Unit, modifier: Modifier = Modifier) {
    val pila = rememberNavBackStack(ListaKey)
    val actual = pila.lastOrNull()
 
    Scaffold(
        modifier = modifier,
        contentWindowInsets = WindowInsets(0),
        bottomBar = {
            AnimatedVisibility(
                visible = actual in PESTANAS,
                enter = slideInVertically { it },
                exit = slideOutVertically { it },
            ) {
                NavigationBar {
                    listOf(ListaKey to "Partidos", TablasKey to "Tablas", RecomposicionKey to "Recomp.")
                        .forEach { (clave, titulo) ->
                            NavigationBarItem(
                                selected = actual == clave,
                                onClick = { irAPestana(pila, clave) },
                                icon = {},
                                label = { Text(titulo) },
                            )
                        }
                }
            }
        },
    ) { paddingValues ->
        NavDisplay(
            modifier = Modifier.padding(paddingValues),
            backStack = pila,
            onBack = { if (pila.size > 1) pila.removeAt(pila.lastIndex) },
            entryDecorators = listOf(
                rememberSaveableStateHolderNavEntryDecorator(),
                rememberViewModelStoreNavEntryDecorator(),
            ),
            entryProvider = entryProvider {
                entry<ListaKey> {
                    val vm: ListaViewModel = hiltViewModel()
                    val partidos by vm.partidos.collectAsStateWithLifecycle()
                    Scaffold(
                        topBar = { CenterAlignedTopAppBar(title = { Text("Partidos") }) },
                        contentWindowInsets = WindowInsets(0),
                    ) { interior ->
                        PantallaPartidos(partidos, onElegir = { id -> pila.add(PartidoKey(id)) }, modifier = Modifier.padding(interior))
                    }
                }
                entry<TablasKey> {
                    val vm: ListaViewModel = hiltViewModel()
                    val torneo by vm.torneo.collectAsStateWithLifecycle()
                    Scaffold(
                        topBar = { CenterAlignedTopAppBar(title = { Text("Tablas") }) },
                        contentWindowInsets = WindowInsets(0),
                    ) { interior ->
                        PantallaTablas(torneo.tablaDePosiciones(), torneo.goleadoresConGoles(), Modifier.padding(interior))
                    }
                }
                entry<RecomposicionKey> {
                    val vm: ListaViewModel = hiltViewModel()
                    val torneo by vm.torneo.collectAsStateWithLifecycle()
                    Scaffold(
                        topBar = { CenterAlignedTopAppBar(title = { Text("Recomposición") }) },
                        contentWindowInsets = WindowInsets(0),
                    ) { interior -> PantallaRecomposicion(torneo, Modifier.padding(interior)) }
                }
                entry<PartidoKey> {
                    val vm: PartidoViewModel = hiltViewModel()
                    PantallaMarcador(
                        onVerDemos = onVerDemos,
                        onVerGoleadores = { pila.add(GoleadoresKey) },
                        onVolver = { pila.removeAt(pila.lastIndex) },
                        onProvocarError = { pila.add(PartidoKey(99)) },
                        viewModel = vm,
                    )
                }
                entry<GoleadoresKey> {
                    val vm: GoleadoresViewModel = hiltViewModel()
                    val goleadores by vm.goleadores.collectAsStateWithLifecycle()
                    PantallaGoleadores(goleadores, alVolver = { pila.removeAt(pila.lastIndex) })
                }
            },
        )
    }
}
 
private fun irAPestana(pila: MutableList<NavKey>, destino: NavKey) {
    if (pila.lastOrNull() == destino) return
    pila.clear()
    if (destino != ListaKey) pila.add(ListaKey)
    pila.add(destino)
}

Comprobación rápida del plugin de serialización

Cuando AppMarcador.kt compile, comprueba que el plugin de la instalación está aplicado. Añade al final del fichero una línea de prueba:

línea de prueba, temporal
private val comprobacion = PartidoKey.serializer()

serializer() es justo lo que genera el plugin. En la Terminal, ./gradlew :app:compileGratisDebugKotlin debe terminar en BUILD SUCCESSFUL. Si dice Unresolved reference 'serializer'., falta alias(libs.plugins.kotlin.serialization) en build.gradle.kts (Module :app) o su Sync Now. Borra la línea de prueba al terminar.

Qué hace cada cambio

  • La clave lleva el id, pero el partido todavía no lo recibe. PartidoKey(3) guarda el 3, y la pila lo conserva aunque el sistema mate el proceso, porque la clave es serializable. Pero PartidoViewModel no tiene por dónde recibirlo: carga siempre el partido 1. Hilt construye ese ViewModel con el repositorio, el caso de uso y las preferencias, y el número del partido no está en ningún módulo. La lección siguiente enseña cómo hacerle llegar un dato así.
  • «Volver» es navegar. «Volver» quita la clave de la pila; «Provocar error» apila PartidoKey(99); de momento esa entrada también abre el partido 1, y el error se verá cuando el ViewModel reciba el número.
  • irAPestana vacía la pila y pone la lista debajo de la pestaña elegida: «atrás» desde Tablas vuelve a la lista y no cierra la app.

Punto de control

  • En la Terminal de Android Studio, ./gradlew :app:assembleGratisDebug termina en BUILD SUCCESSFUL. Para ejecutarla, el botón Run con app en el selector y la variante gratisDebug (el emulador es el de La Activity).
  • Al abrir ves la lista con la barra inferior de tres pestañas: Partidos, Tablas y Recomp. No hay pestaña «Marcador».
  • Toca un partido: aparece la pantalla del partido, sin la barra de pestañas, y dice Partido n.º 1 aunque hayas tocado otro: es lo esperado, el número llega en la lección siguiente. «Provocar error (partido 99)» abre otra pantalla del partido, y «atrás» vuelve a la anterior. «Ver goleadores» abre la tercera pantalla con jugadores y goles: con los datos de ejemplo la primera fila es Nico con 11 goles, la misma cifra que ves en Tablas. Si en lecciones anteriores marcaste goles a mano con «Gol Rayo FC», esos también están en la base de Room, así que las cifras pueden ser mayores que 11 (o la primera fila otro jugador); lo que debe cumplirse es que las dos pantallas coincidan. «Atrás» vuelve al partido y otro «atrás» a la lista.
  • Muerte del proceso: entra en un partido y en goleadores, pulsa Home y ejecuta en la Terminal adb shell am kill com.sebastiannarvaez.marcador.debug (con la app en segundo plano; mira SavedStateHandle). Reábrela desde recientes: aparece la pantalla de goleadores y «atrás» lleva al partido, porque las claves son serializables.
  • Al volver de un partido, la lista conserva su scroll. Si lo pierdes, rememberSaveableStateHolderNavEntryDecorator() debe ir primero en entryDecorators.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.Al pulsar la pestaña «Tablas» desde la lista, irAPestana vacía la pila y deja la lista debajo. ¿Qué se consigue con eso?

  2. 2.¿Por qué PantallaMarcador recibe onVolver en lugar de quitar ella misma la clave de la pila?

  3. 3.En Marcador, «Provocar error (partido 99)» apila PartidoKey(99). ¿Qué ocurre y por qué «Volver» funciona?

  4. 4.Al tocar el partido 3 de la lista, la pila recibe PartidoKey(3), pero la pantalla del partido sigue diciendo Partido n.º 1. ¿Por qué?

  5. 5.¿Qué consigue rememberViewModelStoreNavEntryDecorator() en entryDecorators?

Faltan 5 de 5