remember vs rememberSaveable vs derivedStateOf — ¿cuándo usás cada uno?
Los tres cachean un valor entre recomposiciones, pero resuelven problemas distintos — sobrevivir una rotación, sobrevivir la muerte del proceso, o evitar trabajo desperdiciado.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
remember vs rememberSaveable vs derivedStateOf — ¿cuándo usás cada uno?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Los tres cachean — la pregunta es contra qué
Las tres APIs guardan un valor para que sobreviva a algo que una variable común no sobreviviría, pero cada una responde una pregunta distinta de "¿sobrevive a qué?". remember responde "sobrevive a la recomposición de la misma composición". rememberSaveable responde "sobrevive a la recomposición, a los cambios de configuración y a la muerte del proceso". derivedStateOf directamente no responde una pregunta de supervivencia — responde "cómo evito que las lecturas de una entrada que cambia seguido recompongan algo que en realidad no lo necesita".
#remember: una casilla en la Composition, nada más
remember { ... } guarda su valor en una casilla dentro de la tabla de slots de la Composition, indexada por la posición del punto de llamada. Mientras el composable siga en la composición — no se saque del árbol, y la composición entera no se destruya — el valor sobrevive a cada recomposición, porque recomponer vuelve a correr el cuerpo de la función pero no reconstruye la tabla de slots desde cero. En el momento en que el composable sale de la composición (su rama de if deja de tomarse, su posición en una lista desaparece, o la Activity que lo aloja se destruye y se recrea), la casilla se descarta y el valor desaparece.
Ese último caso es el que hace tropezar a la gente: un cambio de configuración como una rotación de pantalla destruye y recrea la Activity por defecto, lo que tira abajo y reconstruye toda la Composition. Un contador remember { mutableStateOf(0) } vuelve a 0 con la rotación — no porque Compose sea poco confiable, sino porque "la composición se destruyó" es explícitamente uno de los casos que remember nunca fue diseñado para sobrevivir.
#rememberSaveable: la misma casilla, más un Bundle
rememberSaveable hace todo lo que hace remember, y además serializa el valor en el SavedStateRegistry — el mismo mecanismo detrás de Activity.onSaveInstanceState. Eso le da dos garantías extra que remember no tiene: sobrevive un cambio de configuración, y sobrevive a la muerte del proceso (el sistema operativo mata tu app en segundo plano por presión de memoria y después la restaura), porque el Bundle guardado lo escribe el sistema operativo independientemente de que tu proceso siga vivo.
Por defecto funciona de fábrica solo para tipos que entran en un Bundle: primitivos, String, y cualquier cosa Parcelable o Serializable. Para cualquier otra cosa, le das un Saver que sabe cómo aplanar tu tipo en algo compatible con Bundle y reconstruirlo después.
data class FilterState(val query: String, val onlyFavorites: Boolean)val FilterStateSaver = Saver<FilterState, List<Any>>(save = { listOf(it.query, it.onlyFavorites) },restore = { FilterState(query = it[0] as String, onlyFavorites = it[1] as Boolean) },)@Composablefun SearchScreen() {var filter by rememberSaveable(stateSaver = FilterStateSaver) {mutableStateOf(FilterState(query = "", onlyFavorites = false))}// filter ahora sobrevive tanto la rotación COMO la muerte del proceso,// usando el mismo mecanismo basado en Bundle que onSaveInstanceState.}
mapSaver y listSaver son los atajos incorporados para este mismo patrón cuando tu tipo es un simple contenedor de datos — recurrí a un Saver totalmente custom solo cuando la forma del dato realmente lo justifica.
#derivedStateOf: no se trata de sobrevivir a nada
derivedStateOf resuelve un problema completamente distinto: un valor calculado a partir de un estado que cambia mucho más seguido de lo que cambia el resultado calculado en sí. El ejemplo de manual es LazyListState.firstVisibleItemIndex, que cambia con cada píxel scrolleado, alimentando un booleano como "¿debería verse el botón de volver arriba?" que solo cambia un puñado de veces por sesión. Leer firstVisibleItemIndex directamente dentro de un composable lo recompone en cada tick de scroll. Envolver el cálculo en derivedStateOf sigue reevaluando la lambda en cada tick — esa parte no es gratis — pero solo notifica a quienes leen el State derivado cuando el resultado calculado realmente cambia.
@Composablefun ScrollToTopFab(listState: LazyListState, onClick: () -> Unit) {val showButton by remember {derivedStateOf { listState.firstVisibleItemIndex > 0 }}AnimatedVisibility(visible = showButton) {FloatingActionButton(onClick = onClick) {Icon(Icons.Default.ArrowUpward, contentDescription = "Volver arriba")}}}
Hay dos cosas que los ingenieros se equivocan sistemáticamente acá. Primero, derivedStateOf casi siempre necesita ir envuelto en su propio remember — sin eso, se crea un objeto derivedStateOf completamente nuevo (sin memoria del último valor calculado) en cada recomposición, lo que anula toda la optimización. Segundo, derivedStateOf no es una herramienta de memoización de propósito general para "cualquier cosa que calculo a partir de estado" — tiene su propio costo de contabilidad interna (rastrear qué estado de snapshot leyó la lambda, más un chequeo de igualdad en cada lectura), así que para algo barato como "$firstName $lastName", un val simple calculado inline durante la composición es más simple y da lo mismo de rápido. Recurrí a derivedStateOf específicamente cuando la entrada cambia mucho más seguido que la salida derivada, y esa salida alimenta composables que de otro modo recompondrían de forma innecesariamente frecuente.
#Ciclo de vida, lado a lado
#Tabla de decisión
| API | Sobrevive recomposición | Sobrevive cambio de config | Sobrevive muerte del proceso | Uso típico |
|---|---|---|---|---|
remember | Sí | No | No | Estado descartable, solo de UI: una bandera de expandido/colapsado, un Animatable de una animación, un LazyListState que no necesitás restaurar |
rememberSaveable | Sí | Sí | Sí | Cualquier cosa que al usuario le molestaría perder: texto de un formulario, una pestaña seleccionada, una posición de scroll que le importa |
derivedStateOf (normalmente dentro de remember) | Sí (igual que remember) | No | No | Un valor calculado que cambia menos seguido que sus entradas, leído por composables sensibles a la recomposición innecesaria |
🧠 Check your understanding
0/3 · 0/3 answered1. Una pantalla rota. ¿Cuál de estos valores sobrevive a la rotación?
2. ¿Por qué derivedStateOf casi siempre necesita ir envuelto en remember?
3. Necesitás guardar un objeto grande y complejo que debe sobrevivir tanto la rotación como la muerte del proceso. ¿Cuál es la mejor opción?