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)