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 interfacedeclara un conjunto cerrado de tipos: solo valen los que declares tú, y por eso todas las pantallas pueden ser «unaClave». Cada pantalla sin datos es undata objecty cada una con datos, unadata class. mutableListOf<Clave>(Lista)es una lista que se puede modificar, aquí conListadentro desde el principio.pila.lastOrNull()es el último elemento, onullsi la lista está vacía.
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
falseVer solución
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ú.
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):
MarcadorUiState.kt:
PantallaGoleadores.kt (nuevo):
Qué cambia en cada uno, para que no sea una caja negra:
MarcadorUiStatepierde el eventoElegir: elegir un partido ya no es un evento delViewModel, es navegar. Por esoPartidoViewModelpierde también la rama que lo atendía (un cambio de una línea, abajo).GoleadoresViewModelyPantallaGoleadoresson nuevos. ElViewModeles corto: pide los goleadores al repositorio y los expone comoStateFlow.
@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
// 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:
- Borra el
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel: la pantalla deja de pedir suViewModelella misma. - En
PantallaMarcador, añade las tres acciones trasonVerDemos, y quita el valor por defecto deviewModel, que erahiltViewModel()(ahora se lo daAppMarcador, con el id del partido). - Pasa las tres acciones a
PantallaMarcadorContenidoen la llamada del final. - Añade las mismas tres acciones a los parámetros de
PantallaMarcadorContenido. - En la rama
Error, el botón pasa a ser «Volver» y avisa hacia arriba. - En el texto del partido y justo debajo, el botón de ver los goleadores.
- El botón «Provocar error (partido 99)» avisa hacia arriba en lugar de pedirle otro partido al
ViewModel. - La vista previa pasa las tres acciones vacías.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// 1. Borra esta línea de los import:
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel// 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,
) {// 3. La última línea de PantallaMarcador (la llamada a PantallaMarcadorContenido)
PantallaMarcadorContenido(estado, pausas, viewModel::alEvento, onVerDemos, onVerGoleadores, onVolver, onProvocarError, modifier)// 4. Los parámetros de PantallaMarcadorContenido: tres nuevos, después de onVerDemos
onVerDemos: () -> Unit,
onVerGoleadores: () -> Unit,
onVolver: () -> Unit,
onProvocarError: () -> Unit,
modifier: Modifier = Modifier,// 5. En la rama Error, sustituye la línea del botón «Volver al partido 1»
Button(onClick = onVolver) { Text("Volver") }// 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") }// 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)") }// 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, onullsi la pila está vacía (add,removeAtyclear()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@OptInel compilador se niega.- Un
Scaffolddentro de otro: el de fuera pone la barra inferior y los de dentro su barra superior; todos concontentWindowInsets = 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); conWindowInsets(0)elScaffoldno 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):
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):
- 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.
- 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.
- La pantalla que dibuja la pila, con su estado guardado y sus
ViewModelpropios 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
ViewModelde 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.
- Partidos, Tablas y Recomp.: las tres pantallas de antes, cada una con su barra superior y su
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):
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.SerializablePrimera mitad: las cinco claves y la lista de pestañas.
@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.
@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:
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. PeroPartidoViewModelno tiene por dónde recibirlo: carga siempre el partido 1. Hilt construye eseViewModelcon 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 elViewModelreciba el número. irAPestanavací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:assembleGratisDebugtermina enBUILD SUCCESSFUL. Para ejecutarla, el botón Run conappen el selector y la variantegratisDebug(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.º 1aunque 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 esNicocon11 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 enentryDecorators.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Al pulsar la pestaña «Tablas» desde la lista,
irAPestanavacía la pila y deja la lista debajo. ¿Qué se consigue con eso?2.¿Por qué
PantallaMarcadorrecibeonVolveren lugar de quitar ella misma la clave de la pila?3.En Marcador, «Provocar error (partido 99)» apila
PartidoKey(99). ¿Qué ocurre y por qué «Volver» funciona?4.Al tocar el partido 3 de la lista, la pila recibe
PartidoKey(3), pero la pantalla del partido sigue diciendoPartido n.º 1. ¿Por qué?5.¿Qué consigue
rememberViewModelStoreNavEntryDecorator()enentryDecorators?