En Jetpack Compose, las funciones de componibilidad suelen contener el estado con la función remember. Los valores que se recuerdan se pueden reutilizar en las recomposiciones, como se explica en El estado y Jetpack Compose.
Si bien remember sirve como herramienta para conservar valores en las recomposiciones, el estado suele necesitar vivir más allá de la vida útil de una composición. En esta página, se explica la diferencia entre las APIs de remember, retain, rememberSaveable y rememberSerializable, cuándo elegir cada API y cuáles son las prácticas recomendadas para administrar los valores recordados y retenidos en Compose.
Elige la vida útil correcta
En Compose, hay varias funciones que puedes usar para conservar el estado en todas las composiciones y más allá: remember, retain, rememberSaveable y rememberSerializable. Estas funciones difieren en su vida útil y semántica, y cada una es adecuada para almacenar tipos específicos de estado. Las diferencias se describen en la siguiente tabla:
|
|
|
|
|---|---|---|---|
¿Los valores sobreviven a las recomposiciones? |
✅ |
✅ |
✅ |
¿Los valores sobreviven a las recreaciones de actividades? |
❌ |
✅ Siempre se devolverá la misma instancia ( |
✅ Se devolverá un objeto equivalente ( |
¿Los valores sobreviven al cierre del proceso? |
❌ |
❌ |
✅ |
Tipos de datos admitidos |
Todos |
No debe hacer referencia a ningún objeto que se filtraría si se destruye la actividad. |
Debe ser serializable |
Casos de uso |
|
|
|
remember
remember es la forma más común de almacenar el estado en Compose. Cuando se llama a remember por primera vez, se ejecuta el cálculo proporcionado y se recuerda, lo que significa que Compose lo almacena para que la función de componibilidad lo reutilice en el futuro. Cuando un elemento componible se vuelve a componer, ejecuta su código de nuevo, pero cualquier llamada a remember devuelve sus valores de la composición anterior en lugar de volver a ejecutar el cálculo.
Cada instancia de una función de componibilidad tiene su propio conjunto de valores recordados, lo que se conoce como memorización posicional. Cuando los valores recordados se memorizan para usarlos en las recomposiciones, se vinculan a su posición en la jerarquía de composición. Si se usa un elemento componible en diferentes ubicaciones, cada instancia de la jerarquía de composición tiene su propio conjunto de valores recordados.
Cuando ya no se usa un valor recordado, se olvida y se descarta su registro. Los valores recordados se olvidan cuando se quitan de la jerarquía de composición (incluido cuando se quita un valor y se vuelve a agregar para moverlo a una ubicación diferente sin el uso de la función de componibilidad key o MovableContent) o cuando se llama con diferentes parámetros de key.
De las opciones disponibles, remember tiene la vida útil más corta y olvida los valores antes que las otras tres funciones de memorización que se describen en esta página.
Esto hace que sea más adecuado para lo siguiente:
- Crear objetos de estado interno, como la posición de desplazamiento o el estado de la animación
- Evita la recreación costosa de objetos en cada recomposición
Sin embargo, debes evitar lo siguiente:
- Almacenar cualquier entrada del usuario con
remember, ya que los objetos recordados se olvidan en los cambios de configuración de la actividad y el cierre del proceso iniciado por el sistema
rememberSaveable y rememberSerializable
rememberSaveable y rememberSerializable se basan en remember. Tienen la vida útil más larga de las funciones de memorización que se analizan en esta guía.
Además de memorizar objetos de forma posicional en las recomposiciones, también puede
guardar valores para que se puedan restablecer en las recreaciones de actividades,
incluidos los cambios de configuración y el cierre del proceso (cuando el sistema finaliza
el proceso de tu app mientras está en segundo plano, por lo general, para liberar memoria
para las apps en primer plano o si el usuario revoca los permisos de tu app mientras se está
ejecutando).
rememberSerializable funciona de la misma manera que rememberSaveable, pero admite automáticamente la persistencia de tipos complejos que se pueden serializar con la biblioteca de kotlinx.serialization. Elige rememberSerializable si tu tipo está (o puede estar) marcado con @Serializable, y rememberSaveable en todos los demás casos.
Esto hace que tanto rememberSaveable como rememberSerializable sean candidatos perfectos para almacenar el estado asociado con la entrada del usuario, incluida la entrada del campo de texto, la posición de desplazamiento, los estados de activación, etc. Debes guardar este estado para asegurarte de que el usuario nunca pierda su lugar. En general, debes usar rememberSaveable o rememberSerializable para memorizar cualquier estado que tu app no pueda recuperar de otra fuente de datos persistente, como una base de datos.
Ten en cuenta que rememberSaveable y rememberSerializable guardan sus valores memorizados serializándolos en un Bundle. Esto tiene dos consecuencias:
- Los valores que memorices deben poder representarse con uno o más de los siguientes tipos de datos: primitivos (incluidos
Int,Long,FloatyDouble),Stringo arrays de cualquiera de estos tipos. - Cuando se restablece un valor guardado, será una instancia nueva que es igual a (
==), pero no la misma referencia (===) que usó la composición antes.
Para almacenar tipos de datos más complejos sin usar kotlinx.serialization, puedes implementar un Saver personalizado para serializar y deserializar tu objeto en tipos de datos compatibles. Ten en cuenta que Compose comprende los tipos de datos comunes, como
State, List, Map, Set, etc., de forma predeterminada y los convierte
automáticamente en tipos admitidos en tu nombre. A continuación, se muestra un ejemplo de un
Saver para una clase Size. Se implementa empaquetando todas las propiedades de Size en una lista con listSaver.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
La API de retain existe entre remember y rememberSaveable/rememberSerializable en términos de cuánto tiempo memoriza sus valores. Se llama de otra manera porque los valores retenidos también experimentan un ciclo de vida diferente al de sus contrapartes recordadas.
Cuando se conserva un valor, se memoriza de forma posicional y se guarda en una estructura de datos secundaria que tiene un ciclo de vida independiente vinculado al ciclo de vida de la app. Un valor retenido puede sobrevivir a los cambios de configuración sin serializarse, pero no puede sobrevivir al cierre del proceso. Si no se usa un valor después de que se vuelve a crear la jerarquía de composición, el valor retenido se retira (que es el equivalente de retain para olvidarse).
A cambio de este ciclo de vida más corto que el de rememberSaveable, retain puede conservar valores que no se pueden serializar, como expresiones lambda, flujos y objetos grandes, como mapas de bits. Por ejemplo, puedes usar retain para administrar un reproductor multimedia (como ExoPlayer) y evitar interrupciones en la reproducción de contenido multimedia durante un cambio de configuración.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain contra ViewModel
En esencia, tanto retain como ViewModel ofrecen una funcionalidad similar en su capacidad de uso frecuente para conservar instancias de objetos en los cambios de configuración. La elección de usar retain o ViewModel depende del tipo de valor que conserves, de cómo se deba definir su alcance y de si necesitas funcionalidad adicional.
Los ViewModel son objetos que, por lo general, encapsulan la comunicación entre las capas de datos y de IU de tu app. Te permiten quitar la lógica de tus
funciones de componibilidad, lo que mejora la capacidad de prueba. Los ViewModel se administran como
singletons dentro de un ViewModelStore y tienen una vida útil diferente de los valores
retenidos. Si bien un ViewModel permanecerá activo hasta que se destruya su ViewModelStore, los valores retenidos se retiran cuando el contenido se quita de forma permanente de la composición (por ejemplo, para un cambio de configuración, esto significa que se retira un valor retenido si se vuelve a crear la jerarquía de la IU y el valor retenido no se consumió después de que se volvió a crear la composición).
ViewModel también incluye integraciones listas para usar para la inserción de dependencias con Dagger y Hilt, integración con SavedState y compatibilidad integrada con corrutinas para iniciar tareas en segundo plano. Esto hace que ViewModel sea un lugar ideal para
iniciar tareas en segundo plano y solicitudes de red, interactuar con otras fuentes de datos
en tu proyecto y, de manera opcional, capturar y conservar el estado de la IU de misión crítica
que se debe retener en los cambios de configuración en ViewModel y
sobrevivir al cierre del proceso.
retain es más adecuado para objetos que tienen un alcance limitado a instancias de elementos componibles específicas y no requieren reutilización ni uso compartido entre elementos componibles hermanos. Mientras que
ViewModel es un buen lugar para almacenar el estado de la IU y realizar tareas en segundo plano,
retain es un buen candidato para almacenar objetos de la infraestructura de la IU, como cachés,
seguimiento de impresiones y estadísticas, dependencias de AndroidView y otros
objetos que interactúan con el SO Android o administran bibliotecas de terceros, como
procesadores de pagos o publicidad.
Para los usuarios avanzados que diseñan patrones de arquitectura de apps personalizados fuera de las recomendaciones de la arquitectura moderna de apps para Android, retain también se puede usar para compilar una API interna similar a ViewModel. Aunque no se ofrece compatibilidad inmediata con corrutinas y estado guardado, retain puede servir como el componente básico para el ciclo de vida de esos elementos similares a ViewModel con estas funciones integradas. Los detalles sobre cómo diseñar un componente de este tipo están fuera del alcance de esta guía.
|
|
|
|---|---|---|
Definición del alcance |
No hay valores compartidos; cada valor se conserva y se asocia con un punto específico en la jerarquía de composición. Conservar el mismo tipo en una ubicación diferente siempre actúa en una instancia nueva. |
Los |
Destrucción |
Cuando se abandona de forma permanente la jerarquía de composición |
Cuando se borra o destruye el |
Funciones adicionales |
Puede recibir devoluciones de llamada cuando el objeto está en la jerarquía de composición o no. |
|
Propiedad de |
|
|
Casos de uso |
|
|
Combina retain y rememberSaveable o rememberSerializable
A veces, un objeto necesita tener una vida útil híbrida de retained y
rememberSaveable o rememberSerializable. Esto puede ser un indicador de que tu objeto debería ser un ViewModel, que puede admitir el estado guardado, como se describe en la guía del módulo de estado guardado para ViewModel.
Es posible usar retain y rememberSaveable o rememberSerializable
de forma simultánea. Combinar correctamente ambos ciclos de vida agrega una complejidad significativa.
Te recomendamos que emplees este patrón como parte de patrones de arquitectura más avanzados y personalizados, y solo cuando se cumplan todas las condiciones siguientes:
- Estás definiendo un objeto compuesto por una combinación de valores que se deben conservar o guardar (p. ej., un objeto que hace un seguimiento de una entrada del usuario y una caché en la memoria que no se puede escribir en el disco).
- Tu estado tiene un alcance limitado a un elemento componible y no es adecuado para el alcance o la vida útil de singleton de
ViewModel.
Cuando se den todos estos casos, te recomendamos que dividas tu clase en tres partes: los datos guardados, los datos retenidos y un objeto "mediador" que no tenga estado propio y delegue en los objetos retenidos y guardados para actualizar el estado según corresponda. Este patrón tiene la siguiente forma:
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
Cuando se separa el estado por vida útil, la separación de responsabilidades y almacenamiento se vuelve muy explícita. Es intencional que los datos guardados no se puedan manipular con los datos retenidos, ya que esto evita una situación en la que se intenta actualizar los datos guardados cuando el paquete savedInstanceState ya se capturó y no se puede actualizar. También permite probar situaciones de recreación probando tus constructores sin llamar a Compose ni simular una recreación de Activity.
Consulta la muestra completa (RetainAndSaveSample.kt) para ver un ejemplo completo de
cómo se puede implementar este patrón.
Memorización posicional y diseños adaptables
Las aplicaciones para Android pueden admitir muchos factores de forma, incluidos teléfonos, dispositivos plegables, tablets y computadoras de escritorio. Las aplicaciones suelen necesitar realizar la transición entre estos factores de forma con diseños adaptables. Por ejemplo, una app que se ejecuta en una tablet puede mostrar una vista de lista y detalles de dos columnas, pero puede navegar entre una página de lista y una página de detalles cuando se presenta en la pantalla más pequeña de un teléfono.
Debido a que los valores recordados y retenidos se memorizan de forma posicional, solo se reutilizan si aparecen en el mismo punto de la jerarquía de composición. A medida que tus diseños se adaptan a diferentes factores de forma, pueden alterar la estructura de tu jerarquía de composición y generar valores olvidados.
Para los componentes listos para usar, como ListDetailPaneScaffold y NavDisplay (de Jetpack Navigation 3), esto no es un problema y tu estado persistirá durante los cambios de diseño. En el caso de los componentes personalizados que se adaptan a los factores de forma, asegúrate de que el estado no se vea afectado por los cambios de diseño. Para ello, realiza una de las siguientes acciones:
- Asegúrate de que siempre se llame a los elementos componibles con estado en el mismo lugar de la jerarquía de composición. Implementa diseños adaptables modificando la lógica de diseño en lugar de reubicar objetos en la jerarquía de composición.
- Usa
MovableContentpara reubicar de forma correcta los elementos componibles con estado. Las instancias deMovableContentpueden mover los valores recordados y retenidos de sus ubicaciones antiguas a las nuevas.
Recuerda las funciones de fábrica
Aunque las IU de Compose se componen de funciones de componibilidad, muchos objetos se incluyen en la creación y organización de una composición. El ejemplo más común de esto son los objetos componibles complejos que definen su propio estado, como LazyList, que acepta un LazyListState.
Cuando definas objetos enfocados en Compose, te recomendamos que crees una función remember para definir el comportamiento de recordatorio previsto, incluidas la vida útil y las entradas clave. Esto permite que los consumidores de tu estado creen con confianza instancias en la jerarquía de composición que sobrevivirán y se invalidarán según lo esperado. Cuando definas una función de fábrica de componibilidad, sigue estos lineamientos:
- Agrega el prefijo
rememberal nombre de la función. De manera opcional, si la implementación de la función depende de que el objeto searetainedy la API nunca evolucionará para depender de una variación diferente deremember, usa el prefijoretainen su lugar. - Usa
rememberSaveableorememberSerializablesi se elige la persistencia de estado y es posible escribir una implementación deSavercorrecta. - Evita los efectos secundarios o la inicialización de valores basados en
CompositionLocalque podrían no ser relevantes para el uso. Recuerda que el lugar donde se crea el estado puede no ser donde se consume.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }