El estado y Jetpack Compose

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       Text(
           text = "Hello!",
           modifier = Modifier.padding(bottom = 8.dp),
           style = MaterialTheme.typography.h5
       )
       OutlinedTextField(
           value = "",
           onValueChange = { },
           label = { Text("Name") }
       )
   }
}

Si ejecutas el código, 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. Tiene que ver con la manera en que 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 que admiten composición

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. En el caso de ExpandingCard, cada vez que cambia expanded, hace que se vuelva a componer ExpandingCard.

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.h5
           )
       }
       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

Jetpack Compose no requiere que uses MutableState<T> para contener el estado. Jetpack Compose admite otros tipos observables. Antes de leer otro tipo observable en Jetpack Compose, debes convertirlo en un State<T> para que Jetpack Compose pueda 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 de un Flow de manera optimizada para ciclos de vida, lo que permite que tu app guarde los recursos innecesarios. Representa el último valor emitido a través de Compose State. Usa esta API como la forma recomendada de recopilar flujos en apps para Android.

    La siguiente dependencia es obligatoria en el archivo build.gradle (el uso de la última versión alfa, 2.6.0-alpha03, para aprovechar la API de collectAsStateWithLifecycle es temporal):

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03"
}
  • Flow: collectAsState()

    collectAsState es similar a collectAsStateWithLifecycle, ya que también recopila valores de Flow y los transforma en Compose State.

    Usa collectAsState para el código independiente de la plataforma en lugar de collectAsStateWithLifecycle, que es solo para Android.

    No se requieren dependencias adicionales para collectAsState, porque están disponibles en compose-runtime.

  • LiveData: observeAsState()

    observeAsState() comienza a observar este elemento LiveData y representa sus valores a través de State.

    La siguiente dependencia es obligatoria en el archivo build.gradle:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.3.2")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.3.2"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.3.2")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.3.2"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.3.2")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.3.2"
}

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, donde T 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, como hace ExpandingCard con onExpand y onCollapse.

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 para ExpandingCard sin estado se puede almacenar en cualquier lugar. Por ejemplo, ahora es posible mover name a un objeto ViewModel.

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.h5
        )
        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.

Cómo restablecer el estado en Compose

Usa rememberSaveable para restablecer el estado de tu IU después de volver a crear una actividad o un proceso. rememberSaveable conserva el estado en todas las recomposiciones. Además, rememberSaveable también lo conserva en toda la recreación de la actividad y el proceso.

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.

Crea paquetes

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"))
    }
}

Cómo administrar el 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 guía de arquitectura sobre contenedores de estado.

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