rememberSaveable — sobrevivir a rotaciones y process death
remember sobrevive a recomposiciones pero no a rotaciones de pantalla ni a process death. rememberSaveable persiste el estado en el Bundle de Android — el mismo mecanismo que onSaveInstanceState.
// Para tipos primitivos y Strings: funciona automáticamente
var query by rememberSaveable { mutableStateOf("") }
var paginaActual by rememberSaveable { mutableIntStateOf(0) }
// Para tipos Parcelable o Serializable: también automático
@Parcelize
data class FiltroActivo(val categoria: String, val precioMax: Double) : Parcelable
var filtro by rememberSaveable { mutableStateOf(FiltroActivo("Todos", 99999.0)) }
// ✗ NO funciona para tipos que no pueden ir en un Bundle:
// var viewModel by rememberSaveable { ... } // no, los ViewModels no son Parcelable
Saver custom — para tipos arbitrarios
Si el estado no es un tipo primitivo ni Parcelable, podés definir cómo serializarlo y deserializarlo:
data class PantallaState(
val tab: Int,
val scrollPosition: Int,
val modoEdicion: Boolean
)
// Saver que convierte a/desde una lista de primitivos
val PantallaStateSaver = Saver<PantallaState, List<Any>>(
save = { state ->
listOf(state.tab, state.scrollPosition, state.modoEdicion)
},
restore = { lista ->
PantallaState(
tab = lista[0] as Int,
scrollPosition = lista[1] as Int,
modoEdicion = lista[2] as Boolean
)
}
)
// Uso:
var estadoPantalla by rememberSaveable(stateSaver = PantallaStateSaver) {
mutableStateOf(PantallaState(tab = 0, scrollPosition = 0, modoEdicion = false))
}
// Alternativa más simple: mapSaver
val PantallaStateSaver = mapSaver(
save = { mapOf("tab" to it.tab, "scroll" to it.scrollPosition, "edicion" to it.modoEdicion) },
restore = { PantallaState(it["tab"] as Int, it["scroll"] as Int, it["edicion"] as Boolean) }
)
// O listSaver:
val PantallaStateSaver = listSaver(
save = { listOf(it.tab, it.scrollPosition, it.modoEdicion) },
restore = { PantallaState(it[0], it[1], it[2]) }
)
Estado en listas — el problema de mutableStateListOf
// mutableStateListOf — observable y mutable, pero vive en la composición
@Composable
fun ListaTareas() {
val tareas = remember { mutableStateListOf("Comprar pan", "Llamar al médico") }
Column {
tareas.forEachIndexed { index, tarea ->
Row {
Text(tarea)
IconButton(onClick = { tareas.removeAt(index) }) {
Icon(Icons.Default.Delete, "Eliminar")
}
}
}
Button(onClick = { tareas.add("Nueva tarea") }) { Text("Agregar") }
}
}
// Regla: mutableStateListOf está bien para estado local simple de la UI
// Para estado que el ViewModel debe conocer → StateFlow<List<T>> en el ViewModel
// El problema del índice en listas mutables:
// Si eliminás el item en index=2 y hay un LaunchedEffect observando index,
// puede operar sobre el item incorrecto
// Preferí identificadores en lugar de índices como clave
produceState — integrar fuentes externas como State
produceState convierte cualquier fuente de datos asíncrona (callback, Flow, LiveData de otra librería) en un State que Compose puede observar:
// Integrar un callback de sistema como State de Compose
@Composable
fun estadoConectividad(): State<Boolean> {
val context = LocalContext.current
return produceState(initialValue = true) {
val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { value = true }
override fun onLost(network: Network) { value = false }
}
connectivityManager.registerDefaultNetworkCallback(callback)
// awaitDispose se llama cuando el composable sale de la composición
awaitDispose {
connectivityManager.unregisterNetworkCallback(callback)
}
}
}
// Uso:
@Composable
fun Banner() {
val conectado by estadoConectividad()
if (!conectado) {
Text("Sin conexión", color = MaterialTheme.colorScheme.error)
}
}
// También útil para cargar datos una sola vez:
@Composable
fun imagenBitmap(url: String): State<Bitmap?> {
return produceState<Bitmap?>(initialValue = null, url) {
value = cargarImagenDesdeUrl(url) // suspend function
}
}
snapshotFlow — de State a Flow
El inverso de collectAsState(): convierte un State de Compose en un Flow de coroutines. Útil para observar estado de Compose desde un LaunchedEffect o para pasar estado de Compose a código que espera un Flow:
@Composable
fun AutoGuardado(contenido: String, onGuardar: suspend (String) -> Unit) {
// snapshotFlow observa el estado de Compose y emite cuando cambia
LaunchedEffect(Unit) {
snapshotFlow { contenido }
.debounce(1000) // esperar 1 segundo sin cambios
.distinctUntilChanged()
.collect { texto ->
onGuardar(texto) // auto-guardar después de 1s de inactividad
}
}
}
// Observar posición de scroll para analytics:
@Composable
fun ListaConAnalytics(viewModel: ProductosViewModel) {
val listState = rememberLazyListState()
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { indice ->
viewModel.registrarScrollPosicion(indice)
}
}
LazyColumn(state = listState) { /* ... */ }
}
Cuándo bajar el estado — la regla del estado mínimo
// La regla: el estado debe vivir en el ancestro común más bajo
// que necesita leerlo o modificarlo
// ❌ MAL — estado innecesariamente alto en la jerarquía
@Composable
fun Pantalla(viewModel: ViewModel) {
// inputQuery vive en el ViewModel aunque solo lo usa SearchBar
val inputQuery by viewModel.inputQuery.collectAsState()
Column {
SearchBar(query = inputQuery, onChange = viewModel::setQuery)
ResultadosBusqueda(resultados = viewModel.resultados)
}
}
// ✓ BIEN — estado local cuando solo un composable lo necesita
@Composable
fun Pantalla(viewModel: ViewModel) {
Column {
// SearchBar maneja su propio texto localmente
// Solo sube al ViewModel cuando el usuario confirma la búsqueda
SearchBar(onBuscar = viewModel::buscar)
ResultadosBusqueda(resultados = viewModel.resultados)
}
}
@Composable
fun SearchBar(onBuscar: (String) -> Unit) {
var query by remember { mutableStateOf("") } // estado local
TextField(
value = query,
onValueChange = { query = it },
trailingIcon = {
IconButton(onClick = { onBuscar(query) }) { // sube al presionar
Icon(Icons.Default.Search, "Buscar")
}
}
)
}
CompositionLocal — estado implícito en el árbol
Para datos que muchos composables necesitan pero que sería tedioso pasar como parámetro en cada nivel (tema, locale, analíticas, configuración del usuario):
// Definir el CompositionLocal
val LocalAnalytics = compositionLocalOf<AnalyticsTracker> {
error("No hay AnalyticsTracker en la composición")
}
// Proveer el valor en el árbol
@Composable
fun App() {
val analytics = remember { AnalyticsTracker() }
CompositionLocalProvider(LocalAnalytics provides analytics) {
// Todos los composables dentro pueden acceder a LocalAnalytics.current
NavHost(...)
}
}
// Consumir en cualquier nivel del árbol sin prop drilling
@Composable
fun BotonComprar(producto: Producto) {
val analytics = LocalAnalytics.current
Button(onClick = {
analytics.logEvento("producto_comprado", mapOf("id" to producto.id))
}) {
Text("Comprar")
}
}
// compositionLocalOf vs staticCompositionLocalOf:
// compositionLocalOf → recompone solo los composables que lo consumen cuando cambia
// staticCompositionLocalOf → recompone TODO el subtree cuando cambia (más eficiente si no cambia)