El estado en Jetpack Compose: Cómo funciona y cómo sobrevivir al repintado
Antonio Leiva

Cuando uno viene del sistema clásico de vistas, cambiar el chip para empezar a pensar en Compose cuesta un poco precisamente por este paso.

En este artículo te quiero dar una introducción a cómo funcionan los estados en Compose y cómo usarlos, pero este es un tema muy extenso que da para muchos posts.

¡Atención! Si quieres acceder más rápido a todo el contenido en vídeo, organizado, con contenido extra, soporte y muchas más sorpresas, puedes apuntarte gratis a mi formación Compose Expert y ver gratis el primer módulo de más de 3 horas con todo lo necesario para empezar.

Si el estado cambia, la UI cambia

Esto es lo primero que tienes que grabarte a fuego: cada vista de tu UI va a depender de un estado, y cada vez que ese estado cambia, la UI también cambia de forma automática.

Esto nos hace que en vez de tener un estado de presentación y otro de vista, tengamos un único estado para ambas cosas, y por tanto no haya que hacer ningún esfuerzo en sincronizar esos dos estados.

La probabilidad de cometer errores se reduce muchísimo.

Ejemplo de sincronización de texto

Aunque en el artículo anterior vimos cómo crear una lista dinámica, vamos a salirnos un poco del ejemplo para buscar uno que requiera de más estado y nos pueda dar una idea un poco mejor de cómo funciona el estado en Compose.

El objetivo es crear un formulario con un campo de texto, un texto que replica lo que escribimos en el campo, y un botón para borrar el contenido del campo:

Para ello, vamos a usar este código. Hay algunos componentes que no hemos explicado, pero vas a ver que son súper sencillos de usar:

@Composable
fun StateSample() {
    var text = ""

    Column(
        verticalArrangement = Arrangement.Center,
        modifier = Modifier
            .fillMaxSize()
            .padding(64.dp)
    ) {
        TextField(
            value = text,
            onValueChange = { text = it },
            modifier = Modifier.fillMaxWidth()
        )
        Text(
            text = text,
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.Yellow)
                .padding(8.dp)
        )
        Button(
            onClick = { text = "" },
            enabled = text.isNotEmpty(),
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Clear")
        }
    }
}

Inicialmente tenemos una variable text en la que vamos a ir guardando las modificaciones del text, tanto cuando se cambia el texto de un TextField como cuando se pulsa el botón para limpiarlo.

Ese texto se usa tanto para mostrar el texto actual del TextField como el del Text.

Si te fijas, todos los componentes son Stateless, o sin estado. Esto se ve muy bien en el TextField, que no guarda en ningún sitio el texto a mostrar y lo va actualizando por sí mismo, sino que hay que proveérselo por el constructor todo el tiempo.

Esto es muy bueno por lo que hablábamos, el texto solo está en un sitio, y no hay instancias replicadas de ese texto en cada sitio que se usa, por tanto no es necesario sincronizar esos valores.

Ejecútalo y pruébalo. ¿Qué pasa? ¡Pues nada!

State y MutableState

Para que un Composable sea consciente de que su estado ha cambiado y se recomponga, los parámetros de composición tienen que usar instancias de State. Si no, con una variable normal, Compose no es capaz de detectar que algo ha cambiado, y por tanto no se lanza la recomposición.

Tenemos dos variantes:

  • State: es un estado inmutable. Normalmente se usa de interfaz para proteger la modificacion del estado desde sitios en los que no queremos.
  • MutableState: se puede tanto leer como modificar su valor

Cuando el valor de un MutableState se modifica, todos los Composables que dependen de ese estado se recomponen, y por tanto, la UI se actualiza basada en ese nuevo estado de forma automática.

Así que vamos a cambiar el código para usar MutableState. Para ello tenemos una función sencilla llamada mutableStateOf() que genera un estado a partir de un valor:

fun StateSample() {
    val text = mutableStateOf("")
    ...
}

Ahora, donde antes usábamos text, ahora tenemos que usar text.value. Por ejemplo:

TextField(
    value = text.value,
    onValueChange = { text.value = it },
    modifier = Modifier.fillMaxWidth()
)

Bueno, pues ya está, ¿no?. Pruébalo otra vez a ver qué pasa. ¡Sigue sin hacer nada!

¿Qué esta pasando aquí?

El uso de remember

Si volvemos a indagar en lo que ocurre cuando el estado se modifica, lo que pasa es que se relanza el código que renderiza el Composable, incluyendo la línea en la que inicializamos el estado.

Por tanto, cada vez que pasamos por ahí, se crea un nuevo MutableState que se vuelve a inicializar con la cadena vacía.

Para solventar esto, Compose provee la función remember(), que lo que hace es recordar ese valor durante cada recomposición. De esa forma, la primera vez se crea y las siguientes recuperará el valor original.

Están sencillo como cubrir la creación anterior del estado con esta función:

val text = remember { mutableStateOf("") }

Solo con este cambio, ya funcionará todo según lo esperado.

Pero aún lo podemos simplificar un poco más. En vez de usar la función remember(), podemos usar un delegado de propiedad, que nos evita usar el value en todas partes:

var text by remember { mutableStateOf("") }

Si ves que te da error la importación, es porque tienes que añadir los siguientes import, ya que el IDE normalmente no lo resuelve bien:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Para que veas un ejemplo de cómo queda, volvemos a la simplicidad del código inicial pero con todo lo necesario para que el estado en Compose funcione:

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.fillMaxWidth()
)

Pruébalo ahora, ¡por funciona!

Bueno… o casi. Prueba a rotar la pantalla y ver qué pasa 😱

Efectivamente, el estado no se guarda en las rotaciones. Pero no te preocupes, que tiene fácil solución. En vez de remember puedes usar rememberSaveable:

var text by rememberSaveable { mutableStateOf("") }

Solo con esto, ya hemos solventado el problema. Una cosa a tener en cuenta es que rememberSaveable solo puede almacenar tipos que se pueden guardar en un Bundle.

Si no es el caso, se puede pasar un Saver que indica cómo hacer la conversión a Bundle y viceversa.

State hoisting (o elevación de estado)

Si te fijas, hemos estado hablando de las bondades de tener Composables stateless (sin estado), pero el nuestro tiene uno.

Esto no siempre tiene por qué ser una mala idea, pero nos puede interesar sacar ese estado por dos razones:

  • Si el estado se necesita para más cosas: Imagina que ese texto lo usan otras vistas u otras partes de nuestra App. Lo ideal es que todas ellas pudieran suscribirse al mismo estado.
  • Si queremos que un ViewModel gestione el estado: hay que extrear ese estado hasta el punto donde se defina el ViewModel.

Hay una técnica conocida como state hoisting, que he decidido traducir (no sé si muy bien o muy mal) como elevación de estado, que consiste en extraer ese estado de las vistas.

Lo bueno de esta técnica es que es muy procedimental, no hay que pensar mucho para conseguirlo. Eso sí, requiere un poco más de código.

¿En qué consiste? En sustituir la creación del estado, por dos argumentos en el Composable. Para un objeto de tipo T, tendríamos:

  • value : T: el valor a mostrar
  • onValueChange: (T) -> Unit: una lambda que representa el evento de modificación del valor.

Si recuerdas, esto ya lo hemos visto antes:

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.fillMaxWidth()
)

El propio TextField está implementado definiendo esa elevación de estado, de tal forma que se le puedan suministrar esos valores.

Vamos a hacer lo mismo con nuestro Composable. En nuestro caso, T es String, así que sustituyendo lo que vimos antes:

@Composable
fun SimpleForm(
    value: String,
    onValueChange: (String) -> Unit
) {
      ...
}

Te dejo aquí todo el código del Composable para que puedas ver los cambios necesarios:

@Composable
fun SimpleForm(
    value: String,
    onValueChange: (String) -> Unit
) {
    Column(
        verticalArrangement = Arrangement.Center,
        modifier = Modifier
            .fillMaxSize()
            .padding(64.dp)
    ) {
        TextField(
            value = value,
            onValueChange = { onValueChange(it) },
            modifier = Modifier.fillMaxWidth()
        )
        Text(
            text = value,
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.Yellow)
                .padding(8.dp)
        )
        Button(
            onClick = { onValueChange("") },
            enabled = value.isNotEmpty(),
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Clear")
        }
    }
}

Llamar a este Composable ya es bastante sencillo:

var text by rememberSaveable { mutableStateOf("") }
SimpleForm(
    value = text,
    onValueChange = { text = it }
)

Pero como no podía ser de otra forma, y ya que este caso es muy habitual, Compose nos permite una forma más sencilla: la desestructuración.

Podemos desestructurar el remember en getter y setter, pasarlos directamente como argumentos de la función:

val (value, onValueChange) = rememberSaveable { mutableStateOf("") }
SimpleForm(
    value = value,
    onValueChange = onValueChange
)

Muy sencillo, ¿verdad?

Como nuestro ejemplo del curso solo tiene contenido estático, no merece la pena añadir nada de esto allí, pero espero que con esta explicación te queden mucho más claras las ideas sobre cómo funciona el estado en Jetpack Compose.

Puedes ver el código en el commit específico del repositorio de GitHub.

Si no te quieres perder nada, quieres recibir las nuevas publicaciones sobre Compose antes que nadie, soporte y otros extra, te recuerdo que puedes apuntarte a la formación Compose Expert, donde hay más de 3 horas de contenido gratuito.

Apúntate ahora a Compose Expert y accede a más de 3h de contenido gratuito, soporte, extras y mucho más totalmente gratis.

Si no, en el siguiente artículo explicaremos cómo añadir elementos básicos del tema Material como la Toolbar, el Floating Action Button, etc.

Quizá también te interese…

Navegación en Jetpack Compose con Navigation Compose

Navegación en Jetpack Compose con Navigation Compose

Jetpack Compose es un cambio de paradigma enorme en muchos aspectos. Cambia la forma de pensar en casi todos los puntos involucrados en el desarrollo de una App Android. Y la navegación no iba a ser menos. ¿Cómo se navega en Jetpack Compose? ¿Qué opciones tenemos?...

5 consejos para estructurar el código en Jetpack Compose

5 consejos para estructurar el código en Jetpack Compose

Escribir la interfaz con código es genial, pero puede ser que pronto se nos vaya de las manos. Aunque la forma de escribir el código de Jetpack Compose es muy natural y relativamente directa, sí que es verdad que en pro de la flexibilidad, también hay algunos...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *