El estado de una app es cualquier valor que puede cambiar con el paso del tiempo. Esta es una definición muy amplia y abarca desde una base de datos de Room hasta una variable de una clase.
Todas las apps para Android muestran un estado al usuario. Estos son algunos ejemplos de estado de las apps para Android:
- Una barra de notificaciones que se muestra cuando no se puede establecer una conexión de red
- Una entrada de blog y los comentarios asociados
- Las animaciones con efectos de propagación en botones que se reproducen cuando un usuario hace clic en ellas
- Las calcomanías que un usuario puede dibujar sobre una imagen
Jetpack Compose te ayuda a definir explícitamente el lugar y la manera en que almacenas y usas el estado en una app para Android. Esta guía se enfoca en la conexión entre el estado y los elementos que admiten composición, y en las API que Jetpack Compose ofrece para trabajar de manera más sencilla con el estado en cuestión.
Estado y composición
Compose es declarativo y, por lo tanto, la única manera de actualizarlo es llamar al mismo elemento que admite composición con argumentos nuevos. Estos argumentos son representaciones del estado de la IU. Cada vez que se actualiza un estado, se produce una recomposición. En consecuencia, elementos, como TextField
, no se actualizan automáticamente de la misma manera que en las vistas imperativas que se basan en XML. A un elemento que admite composición se le debe informar, de manera explícita, el estado nuevo para que se actualice según corresponda.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
Si ejecutas el código y tratas de ingresar texto, verás que no sucede nada. Eso se debe a que TextField
no se actualiza a sí mismo, sino que lo hace cuando cambia su parámetro value
. Esto se debe a cómo funcionan la composición y la recomposición en Compose.
Para obtener más información sobre la composición inicial y la recomposición, consulta Cómo pensar en Compose.
El estado en elementos componibles
Las funciones de componibilidad pueden usar la API de remember
para almacenar un objeto en la memoria. Un valor calculado por remember
se almacena en la composición durante la composición inicial, y el valor almacenado se muestra durante la recomposición.
Se puede usar remember
para almacenar tanto objetos mutables como inmutables.
mutableStateOf
crea un MutableState<T>
observable, que es un tipo observable integrado en el entorno de ejecución de Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Cualquier cambio en value
programa la recomposición de las funciones que admiten composición que lean value
.
Existen tres maneras de declarar un objeto MutableState
en un elemento que admite composición:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
Esas declaraciones son equivalentes y se proporcionan como sintaxis edulcorada para diferentes usos del estado. Elige la que genere el código más fácil de leer en el elemento que admite composición que desees escribir.
La sintaxis del delegado by
requiere las siguientes importaciones:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Puedes usar el valor recordado como un parámetro para otros elementos que admiten composición o incluso como lógica en declaraciones para cambiar los elementos que se muestran. Por ejemplo, si no quieres mostrar el saludo cuando el nombre está vacío, usa el estado en una declaración if
:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
Aunque remember
te ayuda a retener el estado entre recomposiciones, el estado no se retiene entre cambios de configuración. Para ello, debes usar rememberSaveable
. rememberSaveable
almacena automáticamente cada valor que se puede guardar en un Bundle
. Para otros valores, puedes pasar un objeto Saver personalizado.
Otros tipos de estado compatibles
Compose no requiere que uses MutableState<T>
para contener el estado, ya que admite otros tipos observables. Antes de leer otro tipo observable en Compose, debes convertirlo en un State<T>
para que los elementos componibles puedan recomponer automáticamente cuando cambie el estado.
Compose cuenta con funciones para crear State<T>
a partir de tipos observables comunes utilizados en apps para Android: Antes de usar estas integraciones, agrega los artefactos adecuados según se describe a continuación:
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
recopila valores deFlow
de manera optimizada para ciclos de vida, lo que permite que tu app conserve sus recursos. Representa el último valor emitido deState
de Compose. Usa esta API como la forma recomendada de recopilar flujos en apps para Android.La siguiente dependencia es obligatoria en el archivo
build.gradle
(debe ser 2.6.0-beta01 o versiones posteriores):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.5"
}
-
collectAsState
es similar acollectAsStateWithLifecycle
, ya que también recopila valores deFlow
y los transforma en ComposeState
.Usa
collectAsState
para el código independiente de la plataforma en lugar decollectAsStateWithLifecycle
, que es solo para Android.No se requieren dependencias adicionales para
collectAsState
, porque están disponibles encompose-runtime
. -
observeAsState()
comienza a observar este elementoLiveData
y representa sus valores a través deState
.La siguiente dependencia es obligatoria en el archivo
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.3"
}
-
subscribeAsState()
son funciones de extensión que transforman los flujos reactivos de RxJava2 (p. ej.,Single
,Observable
yCompletable
) enState
de Compose.La siguiente dependencia es obligatoria en el archivo
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.3"
}
-
subscribeAsState()
son funciones de extensión que transforman los flujos reactivos de RxJava3 (p. ej.,Single
,Observable
yCompletable
) enState
de Compose.La siguiente dependencia es obligatoria en el archivo
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.3"
}
Con estado frente a sin estado
Un elemento que admite composición y usa remember
para almacenar un objeto crea un estado interno, lo que genera un elemento con estado que admite composición. HelloContent
es un ejemplo de un elemento con estado que admite composición, ya que mantiene y modifica su estado name
de forma interna. Puede ser útil en situaciones en las que no es necesario que el llamador controle el estado, y pueda usar este estado sin tener que administrarlo por su cuenta. Sin embargo, los elementos con estado interno que admiten composición suelen ser menos reutilizables y más difíciles de probar.
Un elemento sin estado que admite composición no mantiene ningún estado. Una manera fácil de lograr este tipo de estado es usar la elevación de estado.
A medida que desarrollas elementos reutilizables que admiten composición, a menudo deseas exponer una versión con estado y otra sin estado del mismo elemento que admite composición. La versión con estado es conveniente para los llamadores a los que no les importa el estado, y la versión sin estado es necesaria para los llamadores que necesitan controlar o elevar el estado.
Elevación de estado
La toma de estado en Compose es un patrón asociado al movimiento del estado a un llamador de un elemento que admite composición a fin de hacer que un elemento sea sin estado. El patrón general para la elevación de estado en Jetpack Compose es reemplazar la variable de estado con dos parámetros:
value: T
: el valor actual que se mostraráonValueChange: (T) -> Unit
: un evento que solicita que cambie el valor, dondeT
es el valor nuevo propuesto
Sin embargo, no estás limitado a onValueChange
. Si hay eventos más específicos adecuados para el elemento que admite composición, deberás definirlos con expresiones lambda.
El estado elevado de esta manera tiene algunas propiedades importantes:
- Fuente única de información: Mover el estado en lugar de duplicarlo garantizará que exista solo una fuente de información. Eso ayuda a evitar errores.
- Encapsulamiento: Solo elementos con estado componibles pueden modificar su estado. Es completamente interno.
- Capacidad de compartir: El estado elevado puede compartirse con varios elementos que admiten composición. Si deseas leer
name
en un elemento componible diferente, la elevación te permitirá hacerlo. - Capacidad de interceptar: Los llamadores a los elementos componibles sin estado pueden decidir ignorar o modificar eventos antes de cambiar el estado.
- Separación: El estado de los elementos componibles sin estado se puede almacenar en cualquier lugar. Por ejemplo, ahora es posible mover
name
a un objetoViewModel
.
En el caso de ejemplo, extraes el name
y el onValueChange
de HelloContent
, y los mueves hacia arriba en el árbol hasta un elemento que admite composición HelloScreen
que llama a HelloContent
.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
Si se toma el estado de HelloContent
, es más fácil entender el elemento que admite composición, volver a utilizarlo en diferentes situaciones y realizar pruebas. HelloContent
está separado de la forma en que se almacena el estado. Esta separación implica que, si modificas o reemplazas HelloScreen
, no necesitas cambiar la forma en que se implementa HelloContent
.
El patrón en el que el estado baja y los eventos suben se llama flujo unidireccional de datos. En este caso, el estado baja de HelloScreen
a HelloContent
y los eventos suben de HelloContent
a HelloScreen
. Si sigues el flujo unidireccional de datos, podrás separar los elementos que admiten composición y muestran el estado de la IU respecto de las partes de la app que almacenan y cambian el estado.
Consulta la página Dónde elevar el estado para obtener más información.
Cómo restablecer el estado en Compose
La API de rememberSaveable
se comporta de manera similar a remember
porque retiene el estado en todas las recomposiciones y también en la recreación de actividades o procesos con el mecanismo de estado de la instancia guardada. Por ejemplo, esto sucede cuando se rota la pantalla.
Maneras de almacenar el estado
Todos los tipos de datos que se agregan a Bundle
se guardan automáticamente. Si deseas guardar algo que no se puede agregar a Bundle
, tienes varias opciones.
Parcelize
La solución más simple es agregar la anotación @Parcelize
al objeto. El objeto se vuelve parcelable y se puede empaquetar. Por ejemplo, este código hace que un tipo de datos City
se vuelva parcelable y lo guarda en el estado.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
Si la alternativa @Parcelize
no es adecuada por algún motivo, puedes usar mapSaver
para definir tu propia regla de conversión de objetos en conjuntos de valores que el sistema pueda guardar en Bundle
.
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
Para evitar tener que definir las leyendas del mapa, también puedes usar listSaver
y emplear sus índices como leyendas:
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
Contenedores de estado en Compose
La elevación de estado simple se puede administrar en las mismas funciones de componibilidad. Sin embargo, si aumenta la cantidad de estado del que se debe realizar un seguimiento o si surge la lógica que se debe aplicar en las funciones que admiten composición, te recomendamos que delegues las responsabilidades de lógica y de estado a otras clases: los contenedores de estado.
Consulta la elevación de estado en la documentación de Compose o, para referencia más general, la página Contenedores de estado y estado de la IU en la guía de arquitectura para obtener información.
Vuelve a activar los cálculos de recuerdos cuando cambian las claves
La API de remember
se usa con frecuencia junto con MutableState
:
var name by remember { mutableStateOf("") }
Aquí, usar la función remember
hace que el valor MutableState
sobreviva a las recomposiciones.
En general, remember
toma un parámetro lambda calculation
. Cuando se ejecuta remember
por primera vez, invoca la lambda calculation
y almacena su resultado. Durante la recomposición, remember
muestra el valor que se almacenó por última vez.
Además del estado de almacenamiento en caché, también puedes usar remember
para almacenar cualquier objeto o resultado de una operación en la composición que sea costosa de inicializar o calcular. Es posible que no desees repetir este cálculo en cada recomposición.
Un ejemplo es la creación de este objeto ShaderBrush
, que es una operación costosa:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
almacena el valor hasta que abandona la composición. Sin embargo, hay una manera de hacer que el valor almacenado en caché sea no válido. La API de remember
también toma un parámetro key
o keys
. Si cambia alguna de estas claves, la próxima vez que se recomponga la función, remember
hace que la caché se vuelva no válida y vuelve a ejecutar el bloque de cálculo lambda. Este mecanismo te permite controlar la vida útil de un objeto en la composición. El cálculo sigue siendo válido hasta que cambian las entradas, en lugar de hasta que el valor recordado salga de la composición.
En los siguientes ejemplos, se muestra cómo funciona este mecanismo.
En este fragmento, se crea un ShaderBrush
y se usa como pintura de fondo de un elemento Box
componible. remember
almacena la instancia ShaderBrush
porque su recreación es costosa, como se explicó anteriormente. remember
toma avatarRes
como el parámetro key1
, que es la imagen de fondo seleccionada. Si cambia avatarRes
, el pincel se recompone con la imagen nueva y se vuelve a aplicar a la Box
. Esto puede ocurrir cuando el usuario selecciona otra imagen como fondo de un selector.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
En el siguiente fragmento, se eleva el estado a una clase de contenedor de estado sin formato MyAppState
. Expone una función rememberMyAppState
para inicializar una instancia de la clase con remember
. Exponer esas funciones para crear una instancia que sobreviva a las recomposiciones es un patrón común en Compose. La función rememberMyAppState
recibe windowSizeClass
, que sirve como parámetro key
para remember
. Si cambia este parámetro, la app necesita recrear la clase contenedora de estado sin formato con el valor más reciente. Esto puede ocurrir si, por ejemplo, el usuario rota el dispositivo.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose usa la implementación equals de la clase para decidir si una clave cambió y, luego, invalida el valor almacenado.
Almacena el estado con claves más allá de la recomposición
La API de rememberSaveable
es un wrapper alrededor de remember
que puede almacenar datos en un Bundle
. Esta API permite que el estado sobreviva no solo a la recomposición, sino también a la recreación de la actividad y la finalización del proceso iniciado por el sistema.
rememberSaveable
recibe parámetros input
para el mismo propósito que remember
recibe keys
. La caché se invalida cuando cambia alguna de las entradas. La próxima vez que se recomponga la función, rememberSaveable
volverá a ejecutar el bloque de cálculo lambda.
En el siguiente ejemplo, rememberSaveable
almacena userTypedQuery
hasta que cambia typedQuery
:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
Más información
Para obtener más información sobre el estado y Jetpack Compose, consulta los siguientes recursos adicionales.
Ejemplos
Codelabs
Videos
Blogs
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo crear la arquitectura de tu IU de Compose
- Cómo guardar el estado de la IU en Compose
- Efectos secundarios en Compose