Estado en Jetpack Compose

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 y rememberSaveable
  • Cómo trabajar con listas y estados con las APIs de mutableStateListOf y toMutableStateList.
  • Cómo usar ViewModel con Compose

Requisitos

Recomendación (opcional)

Qué compilarás

Implementarás una app de bienestar simple:

8271a7b581390845.png

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

  1. Para iniciar un nuevo proyecto de Compose, abre Android Studio.
  2. 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ú.
  3. Para un proyecto nuevo, selecciona Empty Activity en las plantillas disponibles.

Nuevo proyecto

  1. 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 y app/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ódulo app. 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 de stringResource 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.

  1. Crea un archivo WellnessScreen.kt, que represente la pantalla principal, y llama a nuestra función WaterCounter:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   WaterCounter(modifier)
}
  1. Abre el archivo MainActivity.kt. Quita los elementos de componibilidad Greeting y DefaultPreview. Llama al elemento de componibilidad WellnessScreen recién creado dentro del bloque setContent 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()
               }
           }
       }
   }
}
  1. 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.

7ed1e6fbd94bff04.jpeg

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:

f415ca9336d83142.png

  • 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.

e4dfc3bef967e0a1.gif

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.

e4dfc3bef967e0a1.gif

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.

a9d78ead2c8362b6.gif

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.

7d3509d136280b6c.png

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.

677bc0a178670de8.png

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:

1f8e05f6497ec35f.png

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.

1a8f4095e384ba01.gif

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í:

6e8b72a529e8dedd.png

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:

Diagrama de árbol de componentes que muestra el estado inicial de la app, el recuento es 0

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 tanto WellnessTaskItem como el contador Text comenzarán a mostrarse.

Diagrama de árbol de componentes que muestra el cambio de estado cuando se hace clic en el botón Add one. Se muestra texto con una sugerencia y otro con el recuento de vasos.

865af0485f205c28.png

  • Presiona la X del componente WellnessTaskItem (esto genera otra recomposición). showTask ahora es falso, lo que significa que ya no se mostrará WellnessTaskItem.

Diagrama de árbol de componentes que muestra que, cuando se hace clic en el botón de cierre, la tarea de componibilidad desaparece.

82b5dadce9cca927.png

  • Presiona el botón Add one (otra recomposición). showTask recuerda que cerraste WellnessTaskItem 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 muestra count, y todo el código relacionado con WellnessTaskItem, no se invocan y salen de Composition.

ae993e6ddc0d654a.png

  • showTask se olvida porque no se invocó la ubicación del código en la que se llama a showTask. Volviste al primer paso.

  • Presiona el botón Add one para hacer que count sea mayor que 0 (recomposición).

7624eed0848a145c.png

  • Se vuelve a mostrar el elemento componible WellnessTaskItem porque se olvidó el valor anterior de showTask 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.

2c1134ad78e4b68a.gif

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.

bf2e1634eff47697.gif

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.

  1. 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++ })
}

8211bd9e0a4c5db2.png

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.

2cb0dcdbe75dcfbf.png

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.

7fe6ee3d2886abd0.png

  1. 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

  1. 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 el Checkbox. Asegúrate de elevar el estado checked y la devolución de llamada onCheckedChange para que la función no tenga estado.

a0f8724cfd33cb10.png

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")
        }
    }
}
  1. En el mismo archivo, agrega una función de componibilidad con estado WellnessTaskItem que defina una variable de estado checkedState y la pase al método sin estado del mismo nombre. No te preocupes por onClose 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,
   )
}
  1. 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)
  1. 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.

  1. En WellnessTasksList.kt, agrega una función de componibilidad que cree la lista. Define una LazyColumn 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)
        }
    }
}
  1. Agrega la lista a WellnessScreen. Usa un Column 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()
   }
}
  1. Ejecuta la app y pruébala. Ahora deberías poder revisar las tareas, pero no borrarlas. Implementarás eso en una sección posterior.

f9cbc49c960fd24c.gif

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:

  1. Marca cualquier elemento en la parte superior de esta lista (por ejemplo, los elementos 1 y 2).
  2. Desplázate hasta la parte inferior de la lista para que no aparezcan en la pantalla.
  3. Desplázate hacia arriba para ver los elementos que marcaste anteriormente.
  4. 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.

a68b5473354d92df.gif

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

85796fb49cf5dd16.gif

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.

  1. Abre el archivo WellnessScreen.kt. Mueve el método getWellnessTasks a este archivo para poder usarlo. Para crear la lista, primero llama a getWellnessTasks() y, luego, usa la función de extensión toMutableStateList 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") }
  1. 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 lambda onCloseTask (recibiendo un WellnessTask para borrar). Pasa onCloseTask a WellnessTaskItem.

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) })
       }
   }
}
  1. Modifica WellnessTaskItem: Agrega la función lambda onClose como parámetro al WellnessTaskItem 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.

47f4a64c7e9a5083.png

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.

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

  1. Abre el archivo WellnessScreen. Llama a viewModel() para crear una instancia del ViewModel wellnessViewModel, como parámetro del elemento componible Screen, de modo que pueda reemplazarse cuando se pruebe este elemento y se eleve si es necesario. Proporciona WellnessTasksList a la lista de tareas y quita la función a la lambda onCloseTask.
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.

  1. 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)
  1. 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
       }
}
  1. En WellnessScreen, llama al método changeTaskChecked de ViewModel para proporcionar el comportamiento de onCheckedTask 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)
           }
       )
   }
}
  1. Abre WellnessTasksList y agrega el parámetro de la función lambda onCheckedTask para que puedas pasarlo a WellnessTaskItem.
@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) }
           )
       }
   }
}
  1. 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")
       }
   }
}
  1. Ejecuta la app y trata de marcar alguna tarea. Verás que todavía no puedes hacerlo.

1d08ebcade1b9302.gif

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 que checkedState se convierta en MutableState<Boolean>, en lugar de Boolean, 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.

e7cc030cd7e8b66f.gif

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

APIs de referencia