1. Antes de comenzar
En este codelab, se explican los conceptos principales relacionados con el uso de State en Jetpack Compose. Aquí verás el modo en que el estado de la app determina los elementos que se muestran en la IU, cómo Compose actualiza la IU cuando cambia de estado trabajando con diferentes APIs, la optimización de la estructura de nuestras funciones de componibilidad y el uso de ViewModels en un mundo de Compose.
Requisitos previos
- Conocimientos de la sintaxis de Kotlin
- Conocimientos básicos sobre Compose (puedes comenzar con el instructivo de Jetpack Compose)
- Conocimientos básicos sobre
ViewModel
del componente de la arquitectura
Qué aprenderás
- Cómo pensar en el estado y los eventos en una IU de Jetpack Compose
- Cómo usar Compose el estado para determinar qué elementos mostrar en la pantalla
- Qué es la elevación de estado
- Cómo funcionan las funciones de componibilidad con estado y sin estado
- Cómo Compose realiza un seguimiento automático del estado con la API de
State<T>
- Cómo funcionan la memoria y el estado interno en una función de componibilidad con las APIs de
remember
yrememberSaveable
- Cómo trabajar con listas y estados con las APIs de
mutableStateListOf
ytoMutableStateList
. - Cómo usar
ViewModel
con Compose
Requisitos
Recomendación (opcional)
- Leer Acerca de Compose
- Sigue el codelab de conceptos básicos de Jetpack Compose antes de este codelab. En este codelab, haremos un resumen completo del estado.
Qué compilarás
Implementarás una app de bienestar simple:
La app tiene dos funciones principales:
- Un contador de vasos de agua para hacer el seguimiento del consumo de agua
- Una lista de tareas de bienestar para hacer durante el día
Para obtener más asistencia mientras realizas este codelab, consulta el siguiente código:
2. Cómo prepararte
Cómo iniciar un nuevo proyecto de Compose
- Para iniciar un nuevo proyecto de Compose, abre Android Studio.
- Si te encuentras en la ventana Welcome to Android Studio, haz clic en Start a new Android Studio project. Si ya tienes abierto un proyecto de Android Studio, selecciona File > New > New Project en la barra de menú.
- Para un proyecto nuevo, selecciona Empty Activity en las plantillas disponibles.
- Haz clic en Next y configura tu proyecto con el nombre "BasicStateCodelab".
Asegúrate de seleccionar una minimumSdkVersion del nivel de API 21 como mínimo, que es la mínima que admite la API de Compose.
Cuando eliges la plantilla Empty Compose Activity, Android Studio configura lo siguiente en tu proyecto:
- Una clase
MainActivity
configurada con una función de componibilidad que muestra texto en la pantalla - El archivo
AndroidManifest.xml
, que define los permisos, los componentes y los recursos personalizados de tu app - Los archivos
build.gradle.kts
yapp/build.gradle.kts
, que contienen las opciones y dependencias necesarias para Compose
Solución del codelab
Puedes obtener el código de solución de BasicStateCodelab
en GitHub:
$ git clone https://github.com/android/codelab-android-compose
También tienes la opción de descargar el repositorio como archivo ZIP:
Encontrarás el código de la solución en el proyecto BasicStateCodelab
. Te recomendamos seguir el codelab paso a paso a tu propio ritmo y verificar la solución si necesitas ayuda. Durante el codelab, se te presentarán fragmentos de código que deberás agregar a tu proyecto.
3. Estado en Compose
El "estado" de una app es cualquier valor que puede cambiar con el 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:
- Los mensajes más recientes recibidos en una app de chat
- La foto de perfil del usuario
- La posición de desplazamiento en una lista de elementos
Comencemos a escribir tu app de bienestar.
Para simplificar, durante el codelab, harás lo siguiente:
- Podrás agregar todos los archivos Kotlin en el paquete raíz
com.codelabs.basicstatecodelab
del móduloapp
. Sin embargo, en una app de producción, los archivos deben estructurarse de forma lógica en subpaquetes. - Codificarás todas las strings intercaladas en fragmentos. En una app real, se deberían agregar como recursos de strings en el archivo
strings.xml
y se debe hacer referencia a ellas mediante la API destringResource
de Compose.
La primera funcionalidad que debes construir es el contador de vasos de agua para contar la cantidad de vasos con agua que se consumen durante el día.
Crea una función de componibilidad llamada WaterCounter
que contenga un elemento Text
que muestre la cantidad de vasos. La cantidad de vasos debe almacenarse en un valor llamado count
, que ya puedes codificar.
Crea un archivo nuevo WaterCounter.kt
con la función de componibilidad WaterCounter
de la siguiente manera:
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses.",
modifier = modifier.padding(16.dp)
)
}
Crearemos una función de componibilidad que represente toda la pantalla, que tendrá dos secciones: el contador de vasos de agua y la lista de tareas de bienestar. Por ahora, solo agregaremos el contador.
- Crea un archivo
WellnessScreen.kt
, que represente la pantalla principal, y llama a nuestra funciónWaterCounter
:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
- Abre el archivo
MainActivity.kt
. Quita los elementos de componibilidadGreeting
yDefaultPreview
. Llama al elemento de componibilidadWellnessScreen
recién creado dentro del bloquesetContent
de la actividad de la siguiente manera:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicStateCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WellnessScreen()
}
}
}
}
}
- Si ejecutas la app ahora, verás nuestra pantalla básica del contador de vasos de agua con el conteo codificado de vasos de agua.
El estado de la función de componibilidad WaterCounter
es la variable count
. Sin embargo, tener un estado estático no es muy útil, ya que no se puede modificar. Para solucionar esto, debes agregar un Button
para aumentar el recuento y hacer el seguimiento de la cantidad de vasos de agua que se beben durante el día.
Cualquier acción que ocasione la modificación de un estado se denomina "evento", y daremos más información al respecto en la próxima sección.
4. Eventos en Compose
Hablamos sobre el estado como cualquier valor que cambia con el tiempo, por ejemplo, los últimos mensajes recibidos en una app de chat. Pero ¿qué provoca que el estado se actualice? En las apps para Android, el estado se actualiza en respuesta a eventos.
Los eventos son entradas generadas desde el interior o el exterior de una app, como las siguientes:
- El usuario que interactúa con la IU, por ejemplo, presionando un botón
- Otros factores, como los sensores que envían un valor nuevo o las respuestas de la red
Si bien el estado de la app ofrece una descripción de lo que se mostrará en la IU, los eventos son el mecanismo mediante el cual cambia el estado, lo que genera cambios en la IU.
Los eventos notifican a una parte de un programa que ocurrió algo. En todas las apps para Android, hay un bucle de actualización principal de la IU similar al siguiente:
- Evento: El usuario o alguna otra parte del programa generan un evento.
- Estado de actualización: Un controlador de eventos cambia el estado que usa la IU.
- Estado de visualización: Se actualiza la IU para mostrar el estado nuevo.
Para administrar el estado en Compose, debes comprender cómo el estado y los eventos interactúan entre sí.
Ahora, agrega el botón para que los usuarios puedan modificar el estado con más vasos de agua.
Ve a la función de componibilidad WaterCounter
para agregar Button
debajo de nuestra etiqueta Text
. Un objeto Column
te ayudará a alinear verticalmente el elemento Text
con los elementos componibles Button
. Puedes mover el padding externo al elemento de componibilidad Column
y agregar padding adicional en la parte superior de la Button
para que se separe del texto.
La función de componibilidad Button
recibe una función lambda onClick
, que es el evento que tiene lugar cuando se hace clic en el botón. Verás más ejemplos de funciones lambda más adelante.
Cambia count
a var
, en lugar de val
, para que sea mutable.
import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Cuando ejecutes la app y hagas clic en el botón, verás que no sucede nada. Configurar un valor diferente para la variable count
no hará que Compose lo detecte como un cambio de estado, por lo que no sucederá nada. Esto se debe a que no le indicaste a Compose que debe volver a dibujar la pantalla (es decir, "volver a componer" la función de componibilidad) cuando cambia el estado. Deberás corregir eso en el siguiente paso.
5. Memoria en una función de componibilidad
Las apps de Compose transforman datos en IU si llaman a funciones de componibilidad. Nos referimos a la composición como la descripción de la IU que compila Compose cuando ejecuta elementos componibles. Si se produce un cambio de estado, Compose vuelve a ejecutar las funciones de componibilidad afectadas con el nuevo estado, lo que crea una IU actualizada. Esto se denomina recomposición. Compose también analiza qué datos necesita un elemento de componibilidad individual, de modo que solo recomponga los componentes cuyos datos hayan cambiado y omita los que no se vean afectados.
Para ello, Compose necesita saber de qué estado se debe hacer un seguimiento a fin de que, cuando reciba una actualización, pueda programar la recomposición.
Compose tiene un sistema especial de seguimiento de estado que programa las recomposiciones de los elementos componibles que leen un estado en particular. Esto permite que Compose sea detallado y recomponga solo las funciones de componibilidad que deben cambiar, y no toda la IU. Para ello, se realiza un seguimiento no solo de las "escrituras" (es decir, los cambios de estado), sino también de las "lecturas" del estado.
Usa los tipos State
y MutableState
de Compose para que Compose pueda observar el estado.
Compose realiza un seguimiento de cada elemento de componibilidad que lee las propiedades value
del estado y activa una recomposición cuando cambia su value
. Puedes usar la función mutableStateOf
para crear un MutableState
observable. Recibe un valor inicial como un parámetro que está unido a un objeto State
, lo que luego hace que su value
sea observable.
Actualiza el elemento de componibilidad WaterCounter
de modo que count
use la API de mutableStateOf
con 0
como valor inicial. Como mutableStateOf
muestra un tipo MutableState
, puedes actualizar su value
para actualizar el estado, y Compose activará una recomposición en esas funciones en las que se lee su value
.
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
// Changes to count are now tracked by Compose
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Como se mencionó antes, cualquier cambio en count
programa una recomposición de cualquier función de componibilidad que lea el value
de count
automáticamente. En este caso, se recompone WaterCounter
cada vez que se hace clic en el botón.
Si ejecutas la app ahora, volverás a notar que no sucede nada.
La programación de recomposiciones funciona bien. Sin embargo, cuando se produce una recomposición, la variable count
se reinicia en 0, por lo que necesitamos una manera de preservar este valor entre las recomposiciones.
Para ello, podemos usar la función de componibilidad intercalada remember
. Un valor calculado por remember
se almacena en la composición durante la composición inicial, y el valor almacenado se conserva entre recomposiciones.
Por lo general, remember
y mutableStateOf
se usan juntos en funciones de componibilidad.
Hay algunas formas equivalentes de escribir esto como se muestra en la documentación de estado de Compose.
Modifica WaterCounter
y rodea la llamada a mutableStateOf
con la función de componibilidad intercalada remember
:
import androidx.compose.runtime.remember
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
De manera alternativa, podríamos simplificar el uso de count
con las propiedades delegadas de Kotlin.
Puedes usar la palabra clave by para definir count
como variable. Agregar las importaciones de métodos get y set del delegado nos permite leer y mutar count
de forma indirecta sin hacer referencia a la propiedad value
de MutableState
cada vez.
Ahora, WaterCounter
se ve de la siguiente manera:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Elige la que genere la sintaxis más fácil de leer en el elemento componible que desees escribir.
Ahora, examinemos lo que hicimos hasta ahora:
- Se definió una variable que recordamos con el tiempo llamada
count
. - Se creó una pantalla de texto en la que se indica al usuario el número que recordamos.
- Se agregó un botón que aumenta el número que recordamos cuando se hace clic en él.
Esta disposición forma un ciclo de retroalimentación de flujo de datos con el usuario:
- La IU presenta el estado al usuario (el conteo actual se muestra como texto).
- El usuario produce eventos que se combinan con el estado existente para producir un estado nuevo (al hacer clic en el botón, se agrega uno al recuento actual).
Tu contador de vasos está listo y funcionando.
6. IU basada en el estado
Compose es un framework de IU declarativa. En lugar de quitar los componentes de la IU o cambiar su visibilidad cuando cambia el estado, describimos cómo se encuentra la IU en condiciones de estado específicas. Como resultado de que se llama a una recomposición y se actualiza la IU, los elementos componibles podrían terminar entrando o abandonando la composición.
Este enfoque evita la complejidad de actualizar manualmente las vistas como lo haría con el sistema de View. También es menos propenso a errores, ya que no se olvide de actualizar una vista según un estado nuevo, ya que se realiza automáticamente.
Si se llama a una función de componibilidad durante la composición inicial o en las recomposiciones, se dice que está presente en la composición. Una función de componibilidad a la que no se llama, por ejemplo, porque se llama dentro de una sentencia if y no se cumple la condición, estará ausente en la composición.
Puedes obtener más información sobre el ciclo de vida de los elementos componibles en la documentación.
El resultado de la composición es una estructura de árbol que describe la IU.
A continuación, inspeccionarás el diseño de la app que genera Compose con la herramienta Inspector de diseño de Android Studio.
Para demostrarlo, modifica tu código para que muestre la IU según el estado. Abre WaterCounter
y muestra Text
si count
es mayor que 0:
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Para ejecutar la app, abre la herramienta Inspector de diseño de Android Studio y dirígete a Tools > Layout Inspector.
Verás una pantalla dividida: el árbol de componentes a la izquierda y la vista previa de la app a la derecha.
Para navegar por el árbol, presiona el elemento raíz BasicStateCodelabTheme
en el lado izquierdo de la pantalla. Para expandir todo el árbol de componentes, haz clic en el botón Expand all.
Si haces clic en un elemento de la pantalla de la derecha, navegarás al elemento correspondiente del árbol.
Si presionas el botón Add one en la app, ocurrirá lo siguiente:
- El recuento aumentará a 1 y el estado cambiará.
- Se llamará a una recomposición.
- La pantalla se recompondrá con los elementos nuevos.
Cuando examinas el árbol de componentes con la herramienta Inspector de diseño de Android Studio, ahora también verás el elemento Text
componible:
El estado controla qué elementos están presentes en la IU en un momento determinado.
Las diferentes partes de la IU pueden depender del mismo estado. Modifica Button
para que esté habilitado hasta que count
sea 10 y, luego, se inhabilite (y alcances tu objetivo del día). Para ello, usa el parámetro enabled
de Button
.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
...
}
Ejecuta la app ahora. Los cambios en el estado count
determinan si se muestra o no el Text
, y si se habilita o inhabilita el Button
.
7. Cómo recordar en la composición
remember
almacena objetos en la composición y los olvida si la ubicación de origen a la que se llama a remember
no se vuelve a invocar durante una recomposición.
Para visualizar este comportamiento, implementarás la siguiente funcionalidad en la app: cuando el usuario haya bebido al menos un vaso de agua, mostrará una tarea de bienestar que el usuario pueda hacer y que también pueda cerrar. Dado que los elementos de componibilidad deben ser pequeños y reutilizables, crea un nuevo elemento llamado WellnessTaskItem
que muestre la tarea de bienestar en función de una string recibida como parámetro, junto con el botón de ícono Cerrar.
Crea un archivo nuevo WellnessTaskItem.kt
y agrega el siguiente código. Usarás esta función de componibilidad más adelante en el codelab.
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding
@Composable
fun WellnessTaskItem(
taskName: String,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(start = 16.dp),
text = taskName
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
La función WellnessTaskItem
recibe una descripción de la tarea y una función lambda onClose
(al igual que el elemento integrado componible Button
recibe un onClick
).
WellnessTaskItem
se ve así:
A fin de mejorar nuestra app con más funciones, actualiza WaterCounter
para mostrar WellnessTaskItem
cuando count
> 0.
Cuando count
sea mayor que 0, define una variable showTask
que determine si se muestra WellnessTaskItem
e inicialízala como verdadera.
Agrega una nueva sentencia if para mostrar WellnessTaskItem
si showTask
es verdadero. Usa las APIs que aprendiste en las secciones anteriores para asegurarte de que el valor showTask
sobreviva a las recomposiciones.
@Composable
fun WaterCounter() {
Column(modifier = Modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
Usa la función lambda onClose
de WellnessTaskItem
para que, cuando se presione el botón X, la variable showTask
cambie a false
y la tarea ya no se muestre.
...
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
...
Luego, agrega un Button
nuevo con el texto "Clear water count" y colócalo junto al Button
"Add one". Un objeto Row
puede ayudar a alinear los dos botones. También puedes agregar padding a Row
. Cuando se presiona el botón "Clear water count", la variable count
se restablece a 0.
La función de componibilidad WaterCounter
debería verse de la siguiente manera:
import androidx.compose.foundation.layout.Row
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(
onClick = { count = 0 },
Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
Cuando ejecutas la app, la pantalla muestra el estado inicial:
A la derecha, tenemos una versión simplificada del árbol de componentes, que te ayudará a analizar lo que sucede a medida que cambia el estado. count
y showTask
son valores recordados.
Ahora puedes seguir estos pasos en la app:
- Presiona el botón Add one. Eso aumenta el elemento
count
(lo que causa una recomposición) y tantoWellnessTaskItem
como el contadorText
comenzarán a mostrarse.
- Presiona la X del componente
WellnessTaskItem
(esto genera otra recomposición).showTask
ahora es falso, lo que significa que ya no se mostraráWellnessTaskItem
.
- Presiona el botón Add one (otra recomposición).
showTask
recuerda que cerrasteWellnessTaskItem
en las siguientes recomposiciones si sigues agregando vasos.
- Presiona el botón Clear water count para restablecer el valor de
count
a 0 y provocar una recomposición.Text
, que muestracount
, y todo el código relacionado conWellnessTaskItem
, no se invocan y salen de Composition.
showTask
se olvida porque no se invocó la ubicación del código en la que se llama ashowTask
. Volviste al primer paso.
- Presiona el botón Add one para hacer que
count
sea mayor que 0 (recomposición).
- Se vuelve a mostrar el elemento componible
WellnessTaskItem
porque se olvidó el valor anterior deshowTask
cuando dejó la composición anterior.
¿Qué sucede si se requiere que showTask
persista después de que count
vuelva a 0, más de lo que permite remember
? (es decir, incluso si la ubicación del código donde se llama a remember
no se invoca durante una recomposición). Exploraremos cómo corregir estas situaciones y más ejemplos en las siguientes secciones.
Ahora que comprendes cómo se restablecen la IU y el estado cuando salen de Composition, borra el código y vuelve al WaterCounter
que tenías al comienzo de esta sección:
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
8. Cómo restablecer el estado en Compose
Ejecuta la app, agrega unos vasos de agua al mostrador y, luego, rota el dispositivo. Asegúrate de que la configuración de rotación automática del dispositivo esté activada.
Debido a que Activity se vuelve a crear después de un cambio de configuración (en este caso, la orientación), el estado que se guardó se olvida: el contador desaparece y vuelve a 0.
Lo mismo sucede si cambias el idioma, pasas del modo oscuro al claro, o realizas cualquier otro cambio de configuración que provoque que Android vuelva a crear la actividad en ejecución.
Si bien remember
te ayuda a retener el estado entre recomposiciones, no se retiene en todos los cambios de configuración. Para ello, debes usar rememberSaveable
en lugar de remember
.
rememberSaveable
almacena automáticamente cada valor que se puede guardar en un Bundle
. Para otros valores, puedes pasar un objeto Saver personalizado. Para obtener más información, consulta Cómo restablecer el estado en Compose.
En WaterCounter
, reemplaza remember
por rememberSaveable
:
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
Ejecuta la app ahora y prueba con algunos cambios de configuración. Deberías ver que el contador está guardado correctamente.
La recreación de la actividad es solo uno de los casos de uso de rememberSaveable
. Exploraremos otros caso más adelante mientras trabajemos con listas.
Considera usar remember
o rememberSaveable
, según el estado y las necesidades de UX de tu app.
9. Elevación de estado
Un elemento componible que usa remember
para almacenar un objeto contiene un estado interno, lo que genera un elemento componible con estado. Esto es ú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 de componibilidad con estado interno suelen ser menos reutilizables y más difíciles de probar.
Los elementos de componibilidad y que no tienen ningún estado se denominan elementos sin estado. Una forma fácil de crear un elemento componible sin estado es usar la elevación de estado.
La elevación de estado en Compose es un patrón asociado al movimiento de estado a un llamador de un elemento de componibilidad para quitarle el estado al elemento. El patrón general para la elevación de estado en Jetpack Compose es reemplazar la variable de estado con dos parámetros:
- valor: T: Es el valor actual que se mostrará.
- onValueChange: (T) -> Unit: Es un evento que solicita que el valor cambie con un valor T nuevo,
donde este valor representa cualquier estado que podría modificarse.
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.
- Capacidad de compartir: El estado elevado puede compartirse con varios elementos de componibilidad.
- Capacidad de interceptar: Los llamadores a los elementos de componibilidad sin estado pueden decidir ignorar o modificar eventos antes de cambiar el estado.
- Separación: El estado de una función de componibilidad sin estado se puede almacenar en cualquier lugar. Por ejemplo, en un ViewModel.
Intenta implementar esto para el WaterCounter
de modo que pueda beneficiarse de todo lo anterior.
Con estado y sin estado
Cuando se puede extraer todo el estado de una función de componibilidad, la función de componibilidad resultante se denomina sin estado.
Refactoriza el elemento de componibilidad WaterCounter
dividiéndolo en dos partes: contador con estado y sin estado.
La función del StatelessCounter
es mostrar el count
y llamar a una función cuando incrementas el count
. Para ello, sigue el patrón descrito anteriormente y pasa el estado, count
(como parámetro a la función de componibilidad) y una lambda (onIncrement
), a la que se llama cuando se debe incrementar el estado. StatelessCounter
se ve así:
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
StatefulCounter
es propietario del estado. Eso significa que contiene el estado count
y lo modifica cuando llama a la función StatelessCounter
.
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
¡Bien hecho! Elevaste count
de StatelessCounter
a StatefulCounter
.
Puedes conectar esto a tu app y actualizar WellnessScreen
con StatefulCounter
:
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
Como ya se mencionó, la elevación de estado tiene algunos beneficios. Exploraremos variantes de este código para explicar algunos de ellos. No es necesario que copie los siguientes fragmentos en su app.
- Ahora puedes volver a usar el elemento de componibilidad sin estado. Por ejemplo:
Para contar vasos de agua y jugo, recuerdas waterCount
y juiceCount
, pero usas la misma función de componibilidad StatelessCounter
para mostrar dos estados independientes diferentes.
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
Si se modifica juiceCount
, se vuelve a componer StatefulCounter
. Durante la recomposición, Compose identifica qué funciones leen juiceCount
y activa la recomposición solo de esas funciones.
Cuando el usuario presiona para aumentar juiceCount
, se recompone StatefulCounter
, al igual que StatelessCounter
que lee juiceCount
. Sin embargo, el StatelessCounter
que lee waterCount
no se recompone.
- Tu función de componibilidad con estado puede proporcionar el mismo estado a varias funciones de componibilidad.
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
En este caso, si StatelessCounter
o AnotherStatelessMethod
actualiza el recuento, todo se vuelve a componer, lo cual es esperable.
Dado que se puede compartir el estado elevado, asegúrate de pasar solo el estado que los elementos componibles necesitan para evitar recomposiciones innecesarias y aumentar la reutilización.
Para obtener más información sobre el estado y su elevación, consulta la documentación sobre estado de Compose.
10. Trabajo con listas
A continuación, agrega la segunda función de tu app: la lista de tareas de bienestar. Puedes realizar dos acciones con los elementos de la lista:
- Marcar los elementos de la lista para indicar que se completó la tarea
- Quitar de la lista las tareas que el usuario no quiera completar
Configuración
- Primero, modifica el elemento de la lista. Puedes volver a usar el
WellnessTaskItem
de la sección Cómo recordar en la composición y actualizarlo para que contenga elCheckbox
. Asegúrate de elevar el estadochecked
y la devolución de llamadaonCheckedChange
para que la función no tenga estado.
El elemento WellnessTaskItem
componible de esta sección debería verse de la siguiente manera:
import androidx.compose.material3.Checkbox
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- En el mismo archivo, agrega una función de componibilidad con estado
WellnessTaskItem
que defina una variable de estadocheckedState
y la pase al método sin estado del mismo nombre. No te preocupes poronClose
por ahora, ya que puedes pasar una función lambda vacía.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
- Crea un archivo
WellnessTask.kt
para modelar una tarea que contenga un ID y una etiqueta. Defínelo como una clase de datos.
data class WellnessTask(val id: Int, val label: String)
- Para la lista de tareas en sí, crea un archivo nuevo llamado
WellnessTasksList.kt
y agrega un método que genere algunos datos falsos:
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
Ten en cuenta que, en una app real, obtendrás los datos de tu capa de datos.
- En
WellnessTasksList.kt
, agrega una función de componibilidad que cree la lista. Define unaLazyColumn
y elementos del método de lista que creaste. Si necesitas ayuda, consulta la documentación sobre listas.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
@Composable
fun WellnessTasksList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTasks() }
) {
LazyColumn(
modifier = modifier
) {
items(list) { task ->
WellnessTaskItem(taskName = task.label)
}
}
}
- Agrega la lista a
WellnessScreen
. Usa unColumn
para alinear verticalmente la lista con el contador que ya tienes.
import androidx.compose.foundation.layout.Column
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
- Ejecuta la app y pruébala. Ahora deberías poder revisar las tareas, pero no borrarlas. Implementarás eso en una sección posterior.
Restablece el estado del elemento en LazyList
Examina con más detalle algunos aspectos de los elementos componibles WellnessTaskItem
.
checkedState
pertenece a cada elementode componibilidad WellnessTaskItem
de forma independiente, por ejemplo, una variable privada. Cuando cambia checkedState
, solo se vuelve a componer esa instancia de WellnessTaskItem
, no todas las instancias de WellnessTaskItem
en LazyColumn
.
Para poner esto a prueba, sigue estos pasos:
- Marca cualquier elemento en la parte superior de esta lista (por ejemplo, los elementos 1 y 2).
- Desplázate hasta la parte inferior de la lista para que no aparezcan en la pantalla.
- Desplázate hacia arriba para ver los elementos que marcaste anteriormente.
- Observa que están desmarcados.
Hay un problema, como se vio en una sección anterior, que cuando un elemento sale de la composición, el estado que se recordaba se olvida. En el caso de los elementos de LazyColumn
, estos abandonan la composición por completo cuando te desplazas y dejan de verse.
¿Cómo se soluciona este problema? Nuevamente, usa rememberSaveable
. El estado sobrevivirá a la actividad o a la recreación del proceso con el mecanismo de estado de la instancia guardada. Gracias al modo en que rememberSaveable
funciona junto con LazyList
, tus elementos también pueden sobrevivir al salir de la composición.
Solo reemplaza remember
por rememberSaveable
en tu WellnessTaskItem
con estado, y listo:
import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }
Patrones comunes en Compose
Observa la implementación de LazyColumn
:
@Composable
fun LazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
La función de componibilidad rememberLazyListState
crea un estado inicial para la lista mediante rememberSaveable
. Cuando se recrea la actividad, el estado de desplazamiento se mantiene sin que tengas que codificar nada.
Muchas apps necesitan reaccionar y escuchar la posición de desplazamiento, los cambios de diseño de los elementos y otros eventos relacionados con el estado de la lista. Los componentes diferidos, como LazyColumn
o LazyRow
, admiten este caso de uso mediante la elevación de LazyListState
. Puedes obtener más información sobre este patrón en la documentación sobre el estado de las listas
Tener un parámetro de estado con un valor predeterminado proporcionado por una función rememberX
pública es un patrón común en las funciones de componibilidad integradas. Se puede encontrar otro ejemplo en BottomSheetScaffold
, que eleva el estado mediante rememberBottomSheetScaffoldState
.
11. MutableList observable
A continuación, para agregar el comportamiento de quitar una tarea de nuestra lista, el primer paso es hacer que tu lista sea mutable.
El uso de objetos mutables, como ArrayList<T>
o mutableListOf,
, no funcionará en este caso. Estos tipos no notificarán a Compose que los elementos de la lista cambiaron ni programarán una recomposición de la IU. Necesitas una API diferente.
Debes crear una instancia de MutableList
que Compose pueda observar. Esta estructura permite que Compose realice un seguimiento de los cambios para recomponer la IU cuando se agregan o quitan elementos de la lista.
Comienza por definir nuestro MutableList
observable. La función de extensión toMutableStateList()
es la forma de crear un MutableList
observable a partir de un Collection
inmutable o mutable, como List
.
Como alternativa, también puedes usar el método de fábrica mutableStateListOf
para crear la MutableList
observable y, luego, agregar los elementos para tu estado inicial.
- Abre el archivo
WellnessScreen.kt
. Mueve el métodogetWellnessTasks
a este archivo para poder usarlo. Para crear la lista, primero llama agetWellnessTasks()
y, luego, usa la función de extensióntoMutableStateList
que aprendiste antes.
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- Para modificar la función de componibilidad
WellnessTasksList
, quita el valor predeterminado de la lista, ya que esta se eleva al nivel de la pantalla. Agrega un nuevo parámetro de función lambdaonCloseTask
(recibiendo unWellnessTask
para borrar). PasaonCloseTask
aWellnessTaskItem
.
Debes realizar un cambio más. El método items
recibe un parámetro key
. De forma predeterminada, el estado de cada elemento se relaciona con su posición en la lista.
En una lista mutable, esto causa problemas cuando cambia el conjunto de datos, ya que los elementos que cambian de posición pierden cualquier estado recordado.
Para solucionar fácilmente este problema, usa el id
de cada WellnessTaskItem
como clave para cada elemento.
Para obtener más información sobre las claves de elementos de una lista, consulta la documentación.
WellnessTasksList
se verá de la siguiente manera:
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
- Modifica
WellnessTaskItem
: Agrega la función lambdaonClose
como parámetro alWellnessTaskItem
con estado y llámala.
@Composable
fun WellnessTaskItem(
taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
var checkedState by rememberSaveable { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = onClose,
modifier = modifier,
)
}
¡Bien hecho! La funcionalidad está completa y se pueden borrar elementos de la lista.
Si haces clic en la X de cada fila, los eventos llegarán hasta la lista que posee el estado, lo que quitará el elemento de la lista y hará que Compose vuelva a componer la pantalla.
Si intentas usar rememberSaveable()
para almacenar la lista en WellnessScreen
, obtendrás una excepción de entorno de ejecución:
Este error indica que debes proporcionar un ahorro personalizado. Sin embargo, no debes usar rememberSaveable
para almacenar grandes cantidades de datos ni estructuras de datos complejas que requieran serialización o deserialización extensas.
Se aplican reglas similares cuando se trabaja con onSaveInstanceState
de Activity. Puedes obtener más información en la documentación sobre cómo guardar estados de la IU. Si deseas hacerlo, necesitas un mecanismo de almacenamiento alternativo. Puedes obtener más información sobre las diferentes opciones para preservar el estado de la IU en la documentación.
A continuación, veremos la función de ViewModel como encargada del estado de la app.
12. Estado en ViewModel
La pantalla, o estado de la IU, indica qué se debe mostrar en la pantalla (por ejemplo, la lista de tareas). Por lo general, este estado se conecta con otras capas de la jerarquía porque incluye datos de la aplicación.
Si bien el estado de la IU describe lo que se mostrará en la pantalla, la lógica de una app describe cómo esta se comporta y debe reaccionar ante los cambios de estado. Existen dos tipos de lógica: el comportamiento o la lógica de la IU y la lógica empresarial.
- La lógica de la IU se relaciona con cómo mostrar cambios de estado en la pantalla (por ejemplo, la lógica de navegación o las barras de notificaciones).
- La lógica empresarial, en cambio, define qué hacer con los cambios de estado (por ejemplo, realizar un pago o almacenar las preferencias del usuario). Por lo general, esta lógica se ubica en las capas empresariales o de datos, nunca en la capa de la IU.
Los ViewModels proporcionan el estado de la IU y acceso a la lógica empresarial ubicada en otras capas de la app. Además, como sobreviven a los cambios de configuración, tienen una vida útil más larga que la composición. Incluso pueden seguir el ciclo de vida del host del contenido de Compose, es decir, las actividades, los fragmentos o el destino de un gráfico de navegación, si usas Compose Navigation.
Para obtener más información sobre la arquitectura y la capa de la IU, consulta la documentación sobre la capa de la IU.
Migra la lista y quita el método
Si bien en los pasos anteriores se mostró cómo administrar el estado directamente en las funciones de componibilidad, se recomienda mantener la lógica de la IU y la lógica empresarial separadas del estado de la IU y migrarlos a un ViewModel.
Migremos el estado de la IU, la lista, a tu ViewModel y comencemos a extraer la lógica empresarial.
- Crea un archivo
WellnessViewModel.kt
para agregar tu clase ViewModel.
Mueve tu "fuente de datos" getWellnessTasks()
a WellnessViewModel
.
Define una variable interna _tasks
(con toMutableStateList
como lo hiciste antes) y expón tasks
como lista, de modo que no pueda modificarse desde fuera del ViewModel.
Implementa una función remove
simple que delegue a la función integrada de eliminación de la lista.
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- Podemos acceder a este ViewModel desde cualquier elemento de componibilidad llamando a la función
viewModel()
.
Para usar esta función, abre el archivo app/build.gradle.kts
, agrega la siguiente biblioteca y sincroniza las dependencias nuevas en Android Studio:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
Usa la versión 2.6.2
cuando trabajes con Android Studio Giraffe. De lo contrario, consulta aquí la versión más reciente de la biblioteca.
- Abre el archivo
WellnessScreen
. Llama aviewModel()
para crear una instancia del ViewModelwellnessViewModel
, como parámetro del elemento componible Screen, de modo que pueda reemplazarse cuando se pruebe este elemento y se eleve si es necesario. ProporcionaWellnessTasksList
a la lista de tareas y quita la función a la lambdaonCloseTask
.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
viewModel()
muestra un ViewModel
existente o crea uno nuevo dentro del alcance dado. La instancia de ViewModel se retiene mientras el alcance esté activo. Por ejemplo, si el elemento de componibilidad se usa en una actividad, viewModel()
muestra la misma instancia hasta que finaliza la actividad o se cierra el proceso.
Eso es todo. Integraste ViewModel con parte del estado y la lógica empresarial en tu pantalla. Como el estado se mantiene fuera de la composición y el ViewModel lo almacena, las mutaciones de la lista sobreviven a los cambios de configuración.
ViewModel no conservará automáticamente el estado de la app en ninguna situación (por ejemplo, para el cierre del proceso iniciado por el sistema). Para obtener información detallada sobre cómo mantener el estado de la IU de tu app, consulta la documentación.
Migra el estado activado
La última refactorización es migrar el estado y la lógica verificados al ViewModel. De esta manera, el código es más simple y más fácil de probar, con todo el estado administrado por ViewModel.
- Primero, modifica la clase de modelo
WellnessTask
para que pueda almacenar el estado de marcado y establecer el valor falso como predeterminado.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
- En el ViewModel, implementa un método
changeTaskChecked
que reciba una tarea que se modificará con un valor nuevo para el estado de marcado.
class WellnessViewModel : ViewModel() {
...
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
_tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
- En
WellnessScreen
, llama al métodochangeTaskChecked
de ViewModel para proporcionar el comportamiento deonCheckedTask
de la lista. Las funciones deberían verse de la siguiente manera:
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
- Abre
WellnessTasksList
y agrega el parámetro de la función lambdaonCheckedTask
para que puedas pasarlo aWellnessTaskItem.
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked) },
onClose = { onCloseTask(task) }
)
}
}
}
- Limpia el archivo
WellnessTaskItem.kt
. Ya no necesitamos un método con estado, ya que el estado de la casilla de verificación se elevará al nivel de la lista. El archivo solo tiene esta función de componibilidad:
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- Ejecuta la app y trata de marcar alguna tarea. Verás que todavía no puedes hacerlo.
Esto se debe a que Compose realiza un seguimiento de MutableList
para hacer cambios relacionados con la adición y eliminación de elementos. Por ese motivo, funciona la eliminación. Sin embargo, no tiene conocimiento de los cambios en los valores de los elementos de la fila (en nuestro caso, checkedState
), a menos que le indiques que también los realice.
Existen dos opciones para solucionar este problema:
- Cambia la clase de datos
WellnessTask
para quecheckedState
se convierta enMutableState<Boolean>
, en lugar deBoolean
, lo que hace que Compose realice un seguimiento de un cambio de elemento. - Copia el elemento que estás a punto de mutar, quítalo de la lista y vuelve a agregarlo mutado, lo que hará que Compose realice un seguimiento del cambio en la lista.
Ambos enfoques tienen sus ventajas y sus desventajas. Por ejemplo, según su implementación de la lista que uses, quitar y leer el elemento podría ser costoso.
Supongamos que deseas evitar operaciones de lista potencialmente costosas y hacer que checkedState
sea observable, ya que es más eficiente e idiomático de Compose.
Tu WellnessTask
nuevo podría verse así:
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))
Como viste anteriormente, puedes usar propiedades delegadas, lo que da como resultado un uso más simple de la variable checked
para este caso.
Cambia WellnessTask
para que sea una clase en lugar de una clase de datos. Haz que WellnessTask
reciba una variable initialChecked
con el valor predeterminado false
en el constructor. De esa manera, podremos inicializar la variable checked
con el método de fábrica mutableStateOf
y tomar initialChecked
como valor predeterminado.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
Eso es todo. Esta solución funciona y todas las modificaciones sobreviven a los cambios de recomposición y configuración.
Pruebas
Ahora que la lógica empresarial se refactoriza en ViewModel, en lugar de acoplarse dentro de funciones de componibilidad, la prueba de unidades es mucho más simple.
Puedes usar pruebas instrumentadas para verificar el comportamiento correcto de tu código de Compose y que el estado de la IU funcione correctamente. Considera realizar el codelab Pruebas en Compose para aprender a probar tu IU de Compose.
13. Felicitaciones
¡Bien hecho! Completaste correctamente este codelab y aprendiste todas las APIs básicas para trabajar con el estado de una app de Jetpack Compose.
Aprendiste a pensar en el estado y los eventos para extraer elementos de componibilidad sin estado en Compose, y cómo Compose usa las actualizaciones de estado a fin de impulsar el cambio en la IU.
¿Qué sigue?
Consulta los otros codelabs sobre la ruta de aprendizaje de Compose.
Apps de ejemplo
- JetNews muestra las prácticas recomendadas que se explican en este codelab.
Más documentación
- Cómo pensar en Compose
- El estado y Jetpack Compose
- Flujo de datos unidireccional en Jetpack Compose
- Cómo restablecer el estado en Compose
- Descripción general de ViewModel
- Compose y otras bibliotecas