Saltar al contenido
K
Jetpack y arquitecturaPaso a paso38 min de lectura · 8 min de máquina Intermedio

SavedStateHandle

Al terminar: Conservar estado a través de la muerte del proceso con SavedStateHandle.

Al terminar esta lección, en Marcador puedes tocar un partido de la lista y la pestaña «Marcador» pasa a mostrarlo. Después matas el proceso de la app con adb y, al volver, el partido elegido sigue siendo el mismo. El minuto y los goles en vivo, en cambio, empiezan de cero: es lo esperado, y la lección explica por qué.

Importa porque un ViewModel sobrevive a rotar, pero no a la muerte del proceso, y el usuario no distingue entre «la app se cerró sola en segundo plano» y «la app se rompió». Temas: SavedStateHandle y la muerte del proceso.

Qué es SavedStateHandle

Un SavedStateHandle es un mapa de clave a valor que Android guarda en ese mismo Bundle. Lo pides como parámetro del constructor del ViewModel y, cuando el proceso muere, el sistema te devuelve lo que dejaste ahí.

La regla práctica es de tres niveles:

Qué esDónde vaEjemplo en Marcador
Lo poco y barato que reconstruye la pantallaSavedStateHandleel id del partido elegido
Estado de la pantalla que se puede rehacerViewModel (se pierde con la muerte del proceso)minuto, marcador en vivo, goles a mano
Datos que no se pueden perderuna base de datoslos partidos jugados

viewModel() sabe pasarle un SavedStateHandle al constructor de tu ViewModel sin que hagas nada más: basta con declararlo como parámetro.

ejemplo mínimo: guardar un id en el handle (con comentarios)
class DetalleViewModel(private val estadoGuardado: SavedStateHandle) : ViewModel() {  // viewModel() le pasa el handle solo
    // Un StateFlow que lee del handle: cambia cada vez que se escribe la clave.
    val partidoId: StateFlow<Int> = estadoGuardado.getStateFlow("partidoId", 1)  // clave y valor si aún no hay nada
 
    fun elegir(id: Int) {
        estadoGuardado["partidoId"] = id   // el sistema lo conserva si el proceso muere
    }
}

Error típico: guardar el id en una propiedad normal

con el bug: el id vive solo en el ViewModel
class PartidoViewModel : ViewModel() {
    var partidoId = 1          // sobrevive a rotar, pero no a la muerte del proceso
 
    fun elegir(id: Int) { partidoId = id }
}
corregido: el id vive en el handle
class PartidoViewModel(private val estadoGuardado: SavedStateHandle) : ViewModel() {
    val partidoId: StateFlow<Int> = estadoGuardado.getStateFlow("partidoId", 1)
 
    fun elegir(id: Int) { estadoGuardado["partidoId"] = id }
}

El primero se comporta bien al girar el móvil, así que parece correcto. Falla cuando el sistema mata el proceso: el ViewModel nuevo arranca con el valor por defecto y el usuario vuelve a ver el partido 1 sin haberlo pedido. El segundo guarda el id fuera del proceso.

Un parámetro con valor por defecto

En Kotlin, un parámetro puede llevar un valor por defecto con = valor: si quien llama no pasa nada, se usa ese. Una pantalla puede hacer lo mismo con su ViewModel: lector: LectorViewModel = viewModel() quiere decir «si no me das uno, lo pido yo». Quien quiera darle otro (otra pantalla que ya tiene el suyo, o un ensayo) lo pasa y listo. A la izquierda del = está el nombre del parámetro; a la derecha, la función viewModel() de la lección anterior.

Reto · Ejercicio: el lector que no pierde la página

Vas a comprobar qué guarda el estado guardado y qué no. Para mirar la pantalla en el emulador usa Run Preview, la vista previa ejecutada que presentó Estado en Compose (el triángulo verde junto a @Preview).

Crea los ficheros. Crea la subcarpeta como en ViewModel: qué sobrevive: clic derecho sobre com.sebastiannarvaez.marcador → New → Package → añade .ejercicios.lector al final del nombre que aparece ya escrito. Dentro, New → Kotlin Class/File → Class → LectorViewModel, y otra vez → File → PantallaLector.

El ViewModel recibe su estado guardado al crearse:

  • la página sale de ese estado guardado, con la clave «pagina», y es 1 si todavía no hay nada;
  • una función, pasarPagina, la sube en uno escribiendo en el estado guardado;
  • los minutos leyendo viven solo en el ViewModel: empiezan en 0, solo él los cambia y desde fuera solo se leen;
  • una función, sumarMinuto, los sube en uno.

La pantalla recibe el ViewModel como parámetro con valor por defecto: si no se lo pasan, lo pide. Recoge las dos cosas con el ciclo de vida y muestra, uno debajo de otro:

  • «Página N»;
  • un botón «Pasar página»;
  • «Minutos leyendo: N»;
  • un botón «Sumar minuto».

Añade su vista previa.

Lo que debes ver.

  1. El panel de vistas previas mostrará «Render Problem»: un ViewModel que recibe estado guardado solo lo puede crear una pantalla de verdad. Ejecútala con Run Preview.
  2. Pulsa «Pasar página» dos veces y «Sumar minuto» tres: «Página 3» y «Minutos leyendo: 3». Gira el emulador: sigue igual.
  3. Pulsa Home y, en la Terminal de Android Studio, mata el proceso con las tres órdenes de abajo (adb shell las ejecuta dentro del emulador y se explican en Rotación y muerte del proceso; com.sebastiannarvaez.marcador.debug es el id de la variante gratisDebug, como viste en Gradle: flavors y variantes):
adb shell pidof com.sebastiannarvaez.marcador.debug
adb shell am kill com.sebastiannarvaez.marcador.debug
adb shell pidof com.sebastiannarvaez.marcador.debug

La primera imprime el número de proceso; tras am kill, la tercera no imprime nada: el proceso murió. am kill solo mata procesos en segundo plano, por eso hace falta Home antes. 4. Vuelve desde la lista de recientes del emulador: «Página 3» y «Minutos leyendo: 0».

No cierres la app deslizándola fuera de recientes: el estado se descartaría y arrancaría limpia. Al terminar, elige otra vez app en el selector de ejecución de arriba.

Ver solución
app/src/main/java/com/sebastiannarvaez/marcador/ejercicios/lector/LectorViewModel.kt
package com.sebastiannarvaez.marcador.ejercicios.lector
 
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
 
class LectorViewModel(private val estadoGuardado: SavedStateHandle) : ViewModel() {
    // La página va al handle: sobrevive a rotar Y a la muerte del proceso.
    val pagina: StateFlow<Int> = estadoGuardado.getStateFlow(CLAVE_PAGINA, 1)
 
    // Los minutos viven solo en el ViewModel: sobreviven a rotar, no a la muerte del proceso.
    private val minutosActuales = MutableStateFlow(0)
    val minutos: StateFlow<Int> = minutosActuales.asStateFlow()
 
    fun pasarPagina() {
        estadoGuardado[CLAVE_PAGINA] = pagina.value + 1
    }
 
    fun sumarMinuto() {
        minutosActuales.value += 1
    }
 
    companion object {
        const val CLAVE_PAGINA = "pagina"
    }
}
app/src/main/java/com/sebastiannarvaez/marcador/ejercicios/lector/PantallaLector.kt
package com.sebastiannarvaez.marcador.ejercicios.lector
 
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
 
@Composable
fun PantallaLector(modifier: Modifier = Modifier, lector: LectorViewModel = viewModel()) {
    val pagina by lector.pagina.collectAsStateWithLifecycle()
    val minutos by lector.minutos.collectAsStateWithLifecycle()
 
    Column(modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Text("Página $pagina")
        Button(onClick = lector::pasarPagina) { Text("Pasar página") }
        Text("Minutos leyendo: $minutos")
        Button(onClick = lector::sumarMinuto) { Text("Sumar minuto") }
    }
}
 
@Preview(showBackground = true)
@Composable
private fun PantallaLectorPreview() {
    MaterialTheme { PantallaLector() }
}

Sobre el «Render Problem». La vista previa del panel crea los ViewModels con un dueño falso que no sabe entregar un estado guardado, así que no puede crear este. En Run Preview, la pantalla es una de verdad (una ComponentActivity) y sí lo entrega, y lo restaura tras am kill.

Paso · elegir un partido y que sobreviva a am kill

Objetivo. La lista de partidos se puede tocar; al tocar una fila, el ViewModel cambia de partido y la app salta a la pestaña «Marcador», que dice «Partido n.º 2». Con am kill, el partido sigue siendo el 2.

app → kotlin+java → com.sebastiannarvaez.marcador → feature → marcador → ui
PartidoViewModel.ktse modifica
PantallaMarcador.ktse modifica
app → kotlin+java → com.sebastiannarvaez.marcador → feature → partidos → ui
Listas.ktse modifica
app → kotlin+java → com.sebastiannarvaez.marcador → ui
AppMarcador.ktse modifica
Cuatro ficheros que ya tienes, en la vista Android; no hay nada nuevo que crear.

Nada que instalar. SavedStateHandle vive en la librería lifecycle-viewmodel-savedstate, que ya está en tu proyecto: la trae la Activity de Compose, igual que trajo la clase ViewModel en ViewModel: qué sobrevive. No hay nada de Gradle que tocar, así que no hay Sync Now.

Rutas de los bloques. El título de cada bloque de abajo es la ruta del fichero en tu proyecto (app/src/main/java/…/feature/marcador/ui/…, …/feature/partidos/ui/… o …/ui/…); en la vista Android lo encuentras en app → kotlin+java → com.sebastiannarvaez.marcador, en el paquete que dice la ruta.

Dónde va cada trozo (obligatorio; el código está debajo):

FicheroQué hacer
PartidoViewModel.ktEl constructor y las primeras propiedades sustituyen a la cabecera de la clase; trabajos va después de marcador y antes de golAMano; seleccionar va antes de empezar; empezar se sustituye entera; guionDe va justo antes del companion object y, dentro del companion object que ya tienes, se añade solo la línea CLAVE_PARTIDO.
Listas.ktEl import va tras el package; la firma de PantallaPartidos se sustituye por la nueva, y en la llamada a FilaDePartido de cada fila se añade el Modifier.clickable.
AppMarcador.ktDentro de la función AppMarcador, donde están pestana, titulos y torneo al principio, la línea val viewModel… va justo después de val torneo = …; en el when (pestana), la rama 0 -> recibe viewModel como tercer argumento, y en la rama 1 -> la llamada PantallaPartidos(…), desde ) { interior -> PantallaPartidos( hasta su ) } final, se sustituye por la del bloque.
PantallaMarcador.ktLa firma y las dos primeras líneas del cuerpo (val viewModel… y val partido…) se sustituyen por las del bloque, y el Text va como primera línea dentro de extras = { … }.

Piezas nuevas de este paso:

  • SavedStateHandle como parámetro del constructor, handle.getStateFlow(clave, inicial) y handle[clave] = valor: arriba.
  • Job y cancel() (los de Alcance, Job y cancelación): el Job es lo que devuelve launch; el ViewModel guarda el suyo para poder pararlo al cambiar de partido: si no lo cancelaras, el reloj del partido anterior seguiría contando y mezclaría sus goles con los del partido nuevo. Se guarda en una lista (List<Job>) porque forEach { it.cancel() } cancela «todos los que haya» y sirve igual con uno, con varios o con ninguno.
  • Modifier.clickable { … }: hace que un elemento responda al toque.
  • DatosDeEjemplo.partidos.first { it.id == id }: busca en la lista de 18 partidos de Listas con LazyColumn el que tiene ese id. Hasta la lección de Room, esa lista hace de base de datos.
  • viewModel: PartidoViewModel = viewModel(): el parámetro por defecto de antes del ejercicio. AppMarcador le pasa el suyo para que se vea con claridad que lista y pantalla comparten el ViewModel (no haría falta: viewModel() ya devuelve la misma instancia dentro de la misma Activity).

Esas piezas, en un reproductor de música inventado (otra clave, otros datos; no es PartidoViewModel):

ejemplo mínimo: cancelar trabajos, elegir de una lista y una fila clicable (con comentarios)
data class Cancion(val id: Int, val titulo: String)
val canciones = listOf(Cancion(1, "Vamos Rayo"), Cancion(2, "Toros al ataque"))  // los datos de mentira del ejemplo
 
class ReproductorViewModel(private val estadoGuardado: SavedStateHandle) : ViewModel() {
    val cancionId: StateFlow<Int> = estadoGuardado.getStateFlow(CLAVE_CANCION, 1)
    private var trabajos: List<Job> = emptyList()           // los Job que devuelve launch, para poder pararlos
 
    fun elegir(id: Int) {
        if (id == cancionId.value) return                   // la misma: no hay nada que hacer
        trabajos.forEach { it.cancel() }                    // para todo lo que estuviera sonando
        trabajos = emptyList()
        estadoGuardado[CLAVE_CANCION] = id                  // guarda el id: sobrevive a am kill
    }
 
    fun reproducir() {
        trabajos = listOf(viewModelScope.launch { delay(1_000) })  // launch devuelve un Job; se guarda en la lista
    }
 
    fun tituloDe(id: Int): String = canciones.first { it.id == id }.titulo  // first: el primero que cumple
 
    companion object {
        const val CLAVE_CANCION = "cancionId"               // la clave, escrita una sola vez
    }
}
 
@Composable
fun ListaDeCanciones(
    onElegir: (Int) -> Unit,
    modifier: Modifier = Modifier,
    reproductor: ReproductorViewModel = viewModel(),        // parámetro con valor por defecto: si no te lo pasan, lo pide
) {
    val elegida by reproductor.cancionId.collectAsStateWithLifecycle()  // la canción guardada en el handle
    Column(modifier) {
        canciones.forEach { cancion ->                      // una fila por canción
            Text(cancion.titulo, Modifier.clickable { onElegir(cancion.id) })  // clickable: la fila responde al toque
        }
        Text("Suena: ${reproductor.tituloDe(elegida)}")
    }
}

Intenta escribirlo tú.

  1. PartidoViewModel recibe su estado guardado y expone, para leer, el id del partido elegido (que sale de ese estado guardado, con la clave partidoId, y es 1 si no hay ninguno) y el partido, cuyo guion se busca por id en la lista de partidos de ejemplo.
  2. Una función, seleccionar, que recibe un id y, si es otro partido, para el reloj, pone a cero el minuto, el aviso («ninguno») y los dos marcadores, cambia el guion y guarda el id en el estado guardado. empezar juega ahora el guion elegido y guarda su trabajo para poder pararlo.
  3. En Partidos, tocar una fila elige ese partido y salta a la pestaña del marcador; y la pantalla del marcador comparte el ViewModel con la lista y enseña debajo qué partido se eligió. Debes ver «Partido n.º 2 (elegido en la pestaña Partidos)» bajo el marcador. Una cosa cambia a partir de ahora: la pantalla ya no enseña Ejemplo.rayoContraToros por defecto, sino el partido 1 de la lista (también Rayo contra Toros, pero con otro guion de goles).
Intenta primero. Cuando termines, compara con la solución.Mostrar el código

PartidoViewModel.kt. Imports: el de Ejemplo deja de usarse (puedes borrarlo) y se añaden estos cuatro (DatosDeEjemplo, la lista de partidos de la feature partidos, es de otro paquete y por eso lleva import):

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
import androidx.lifecycle.SavedStateHandle
import com.sebastiannarvaez.marcador.feature.partidos.data.DatosDeEjemplo
import com.sebastiannarvaez.marcador.torneo.Partido
import kotlinx.coroutines.Job

La cabecera de la clase y sus primeras propiedades (sustituyen a class PartidoViewModel : ViewModel() {, a val partido = … y a golesEnVivo):

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
class PartidoViewModel(private val estadoGuardado: SavedStateHandle) : ViewModel() {
 
    // Un StateFlow LEIDO DEL HANDLE: cada vez que se escribe `estadoGuardado[CLAVE]`, cambia.
    val partidoId: StateFlow<Int> = estadoGuardado.getStateFlow(CLAVE_PARTIDO, 1)
 
    private val guion = MutableStateFlow(guionDe(partidoId.value))
    private val golesEnVivo = MutableStateFlow(Marcador(0, 0))
 
    val partido: StateFlow<Partido> = guion.asStateFlow()

Después de marcador y antes de golAMano, la lista de trabajos; y seleccionar, justo antes de empezar:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
    private var trabajos: List<Job> = emptyList()
app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
    // Elegir otro partido: para el directo, lo pone a cero y GUARDA el id en el handle.
    fun seleccionar(id: Int) {
        if (id == partidoId.value) return
        trabajos.forEach { it.cancel() }
        trabajos = emptyList()
        minutoActual.value = 0
        enMarcha.value = false
        avisoActual.value = "ninguno"
        golesAMano.value = Marcador(0, 0)
        golesEnVivo.value = Marcador(0, 0)
        guion.value = guionDe(id)
        estadoGuardado[CLAVE_PARTIDO] = id
    }

empezar (sustituye a la anterior; ahora lanza en una lista de trabajos y usa el guion elegido):

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
    fun empezar() {
        if (enMarcha.value || minutoActual.value > 0) return
        enMarcha.value = true
        val partidoActual = guion.value
        trabajos = listOf(
            viewModelScope.launch {
                while (minutoActual.value < MINUTOS_DEL_PARTIDO) {
                    delay(MS_POR_MINUTO)
                    val nuevo = minutoActual.value + 1
                    golesEnVivo.update { antes ->
                        partidoActual.eventos.filter { it.minuto == nuevo }.fold(antes) { m, evento -> m.despuesDe(evento, partidoActual) }
                    }
                    minutoActual.value = nuevo
                    if (nuevo % 15 == 0) {
                        val vivo = golesEnVivo.value
                        val mano = golesAMano.value
                        avisoActual.value = "minuto $nuevo con ${Marcador(vivo.local + mano.local, vivo.visitante + mano.visitante)}"
                    }
                }
                enMarcha.value = false
            },
        )
    }

Justo antes del companion object, guionDe:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
    private fun guionDe(id: Int): Partido = DatosDeEjemplo.partidos.first { it.id == id }.partido

Y, dentro del companion object que ya tienes (el que contiene MS_POR_MINUTO), añade solo esta línea; no pegues otro companion object, porque una clase solo puede tener uno:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PartidoViewModel.kt
        const val CLAVE_PARTIDO = "partidoId"

Listas.kt: el import va arriba, tras el package; PantallaPartidos recibe onElegir; y cada fila se hace clicable:

app/src/main/java/com/sebastiannarvaez/marcador/feature/partidos/ui/Listas.kt
import androidx.compose.foundation.clickable
app/src/main/java/com/sebastiannarvaez/marcador/feature/partidos/ui/Listas.kt
fun PantallaPartidos(
    partidos: List<PartidoDeLista>,
    onElegir: (Int) -> Unit,
    modifier: Modifier = Modifier,
) {
app/src/main/java/com/sebastiannarvaez/marcador/feature/partidos/ui/Listas.kt
                FilaDePartido(
                    item.partido.local.nombre,
                    item.partido.visitante.nombre,
                    item.partido.marcador().toString(),
                    Modifier.clickable { onElegir(item.id) },
                )

AppMarcador.kt: el ViewModel se pide en la raíz, se le pasa a la pantalla del marcador, y la lista, al elegir, llama a seleccionar y salta a la pestaña 0. Los dos import van arriba con los demás (PartidoViewModel vive en otra carpeta de la app, feature → marcador → ui):

app/src/main/java/com/sebastiannarvaez/marcador/ui/AppMarcador.kt
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sebastiannarvaez.marcador.feature.marcador.ui.PartidoViewModel
app/src/main/java/com/sebastiannarvaez/marcador/ui/AppMarcador.kt
    val viewModel: PartidoViewModel = viewModel()
app/src/main/java/com/sebastiannarvaez/marcador/ui/AppMarcador.kt
            0 -> PantallaMarcador(onVerDemos, Modifier.padding(paddingValues), viewModel)
app/src/main/java/com/sebastiannarvaez/marcador/ui/AppMarcador.kt
            ) { interior -> PantallaPartidos(
                DatosDeEjemplo.partidos,
                onElegir = { id ->
                    viewModel.seleccionar(id)
                    pestana = 0
                },
                modifier = Modifier.padding(interior),
            ) }

PantallaMarcador.kt: la función recibe el ViewModel como parámetro y recoge partido y partidoId (sustituye a la firma y a val viewModel… / val partido = …):

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
fun PantallaMarcador(
    onVerDemos: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: PartidoViewModel = viewModel(),
) {
    val partido by viewModel.partido.collectAsStateWithLifecycle()
    val partidoId by viewModel.partidoId.collectAsStateWithLifecycle()

Y, dentro de extras = { … }, como primera línea, el texto que enseña qué partido hay elegido:

app/src/main/java/com/sebastiannarvaez/marcador/feature/marcador/ui/PantallaMarcador.kt
                Text("Partido n.º $partidoId (elegido en la pestaña Partidos)")

Qué hace cada pieza

  • getStateFlow convierte una clave del handle en un StateFlow: al escribir estadoGuardado[CLAVE_PARTIDO] = id, los que lo recogen ven el valor nuevo y el sistema lo guarda fuera del proceso.
  • seleccionar lo rehace todo. Cancela los trabajos del reloj y pone a cero minuto, aviso y goles, porque un partido nuevo empieza de cero y no se arrastra nada del anterior.
  • Solo va el id al handle. Un partido entero no cabe en un Bundle, y no hace falta: con el id, guionDe lo reconstruye.

Mata el proceso y comprueba

Ejecuta la app con Run, con la variante gratisDebug elegida en View → Tool Windows → Build Variants y el emulador arrancado. En la pestaña Partidos toca la segunda fila; la pestaña Marcador dice «Partido n.º 2». Pulsa el botón Home del emulador y mata el proceso con las tres órdenes del ejercicio (pidof, am kill y pidof otra vez, con el id com.sebastiannarvaez.marcador.debug). Abre la app desde la lista de recientes del emulador. Verás «Partido n.º 2» y el minuto en 0'. Si pides el pid otra vez, es otro número: es un proceso nuevo con un ViewModel nuevo, que ha leído el id del handle.

Punto de control

  • La app compila y se ejecuta sin errores en rojo (si prefieres comprobarlo en la Terminal de Android Studio, ./gradlew :app:assembleGratisDebug termina en BUILD SUCCESSFUL).
  • Tocar la segunda fila de «Partidos» lleva a «Marcador» con «Partido n.º 2» y el marcador en 0-0.
  • Tras Home, am kill y volver desde recientes, sigue diciendo «Partido n.º 2», con el minuto en 0'.
  • En ejercicios tienes la subcarpeta lector con sus dos ficheros, y tras am kill mostraba «Página 3» y «Minutos leyendo: 0».
  • PartidoViewModel.kt tiene SavedStateHandle en su constructor y build.gradle.kts (Module :app) no ha cambiado.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.Tras un am kill con la app en segundo plano, ¿qué pasa con el ViewModel que tenía la app?

  2. 2.¿Qué es lo adecuado para guardar en un SavedStateHandle?

  3. 3.Un ViewModel guarda el partido elegido en una propiedad normal, var partidoId = 1 (el partido inicial). El usuario elige el partido 2, gira el móvil y luego el sistema mata el proceso. ¿Qué ve en cada situación?

  4. 4.¿Cómo obtiene un ViewModel su SavedStateHandle cuando se pide con viewModel()?

  5. 5.Con la app en segundo plano matas el proceso con am kill. ¿Cuál de estos datos de Marcador no se recupera al volver?

Faltan 5 de 5