Introducción al estado en Compose

1. Antes de comenzar

En este codelab, aprenderás sobre el estado y cómo se puede usar y modificar con Jetpack Compose.

En esencia, el estado de una app es cualquier valor que puede cambiar con el tiempo. Esta definición es muy amplia y abarca desde una base de datos hasta una variable en tu app. Aprenderás más sobre las bases de datos en una unidad posterior. Sin embargo, por ahora, solo debes saber que una base de datos es un conjunto organizado de información estructurada, como los archivos en tu computadora.

Todas las apps para Android muestran un estado al usuario. Estos son algunos ejemplos de estado de las apps para Android:

  • Un mensaje que se muestra cuando no se puede establecer una conexión de red.
  • Formularios, como formularios de registro. Puedes completar y enviar tu estado.
  • Controles que se pueden presionar, como botones. El estado puede ser no presionado, se está presionando (animación de la pantalla) o presionado (una acción onClick).

En este codelab, explorarás cómo usar el estado y cómo pensar en él a la hora de usar Compose. Para ello, compilarás una app de calculadora de propinas llamada Tip Time con estos elementos integrados de la IU de Compose:

  • Un elemento TextField componible para ingresar y editar texto
  • Un elemento Text componible para mostrar texto
  • Un elemento Spacer componible para mostrar espacio vacío entre los elementos de la IU

Al final de este codelab, habrás creado una calculadora de propinas interactiva que calcula automáticamente el importe de la propina cuando ingresas el importe del servicio. En esta imagen, se muestra cómo se ve la app final:

e82cbb534872abcf.png

Requisitos previos

  • Conocimientos básicos sobre Compose (como la anotación @Composable)
  • Conocimientos básicos sobre diseños de Compose, como los elementos de diseño Row y Column componibles
  • Conocimientos básicos sobre los modificadores, como la función Modifier.padding()
  • Conocimientos sobre el elemento Text componible

Qué aprenderás

  • Cómo pensar en el estado en una IU
  • Cómo Compose usa el estado para mostrar datos
  • Cómo agregar un cuadro de texto a tu app
  • Cómo elevar un estado

Qué compilarás

  • Una app para calcular propinas llamada Tip Time, que te permite calcular un importe según el importe del servicio.

Requisitos

  • Una computadora con acceso a Internet y un navegador web
  • Conocimientos sobre Kotlin
  • La versión más reciente de Android Studio

2. Primeros pasos

  1. Consulta la calculadora en línea de propinas de Google. Ten en cuenta que este es solo un ejemplo y que esta no es la app para Android que crearás en este curso.

46bf4366edc1055f.png 18da3c120daa0759.png

  1. Ingresa valores diferentes en los cuadros Bill (facturación) y Tip % (porcentaje de propina). El valor total y de la propina cambian.

c0980ba3e9ebba02.png

Observa que en el momento en que ingresas los valores, se actualizan las cifras de Propina y Total. Para cuando finalices el siguiente codelab, desarrollarás una app de calculadora de propinas similar en Android.

En esta ruta de aprendizaje, compilarás una app para Android simple para calcular propinas.

Los desarrolladores suelen trabajar de esta manera: tienen una versión simple de la app lista y en funcionamiento (incluso si no tiene muy buen aspecto) y, luego, agregan más funciones y la vuelven más atractiva a nivel visual.

Al final de este codelab, tu app de calculadora de propinas se verá como estas capturas de pantalla. Cuando el usuario ingrese un importe de la factura, la app mostrará un importe sugerido para la propina. Por el momento, el porcentaje de propina está codificado en 15%. En el siguiente codelab, seguirás trabajando en tu app y agregarás más funciones, como la opción de configurar un porcentaje de propina personalizado.

3. Cómo obtener el código de partida

El código de partida es un código escrito previamente que se puede usar como punto de partida para un proyecto nuevo. También puede ayudarte a enfocarte en los conceptos nuevos que se enseñan en este codelab.

Para comenzar a usar el código de partida, descárgalo aquí:

Descargar ZIP

Como alternativa, puedes clonar el repositorio de GitHub para el código:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout starter

Puedes explorar el código de partida en el repositorio TipTime de GitHub.

Descripción general de la app de partida

Para familiarizarte con el código de partida, completa los siguientes pasos:

  1. Abre el proyecto con el código de partida en Android Studio.
  2. Ejecuta la app en un dispositivo Android o en un emulador.
  3. Verás dos componentes de texto: uno es para una etiqueta y el otro es para mostrar el importe de la propina.

e85b767a43c69a97.png

Explicación del código de partida

El código de partida tiene los elementos de texto componibles. En esta ruta de aprendizaje, agregarás un campo de texto para ingresar la entrada del usuario. Esta es una breve explicación de algunos archivos para que puedas comenzar.

res > values > strings.xml

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="bill_amount">Bill Amount</string>
   <string name="tip_amount">Tip Amount: %s</string>
</resources>

Este es el archivo string.xml en los recursos con todas las cadenas que usarás en esta app.

MainActivity

Este archivo contiene principalmente código generado por plantillas y las siguientes funciones.

  • La función TipTimeLayout() contiene un elemento Column con dos elementos de texto componibles y que ves en las capturas de pantalla. También tiene un elemento spacer componible para agregar espacio por razones estéticas.
  • La función calculateTip(), que acepta el importe de la factura y calcula un importe de la propina del 15% El parámetro tipPercent se establece en un valor de argumento predeterminado 15.0. Por ahora, el valor predeterminado de la propina es 15%. En el siguiente codelab, obtendrás el importe de la propina del usuario.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.Start)
        )
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        Spacer(modifier = Modifier.height(150.dp))
    }
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

En el bloque Surface() de la función onCreate(), se llama a la función TipTimeLayout(). Se mostrará el diseño de la app en el dispositivo o el emulador.

override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeLayout()
           }
       }
   }
}

En el bloque TipTimeTheme de la función TipTimeLayoutPreview(), se llama a la función TipTimeLayout(). Se mostrará el diseño de la app en Design y en el panel Split.

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
   TipTimeTheme {
       TipTimeLayout()
   }
}

ae11354e61d2a2b9.png

Cómo obtener información del usuario

En esta sección, agregarás el elemento de la IU que le permite al usuario ingresar el importe de la factura en la app. En esta imagen se puede observar cómo se ve:

58671affa01fb9e1.png

Tu app usa un estilo y un tema personalizados.

Los estilos y los temas son una colección de atributos que especifica la apariencia de un solo elemento de la IU. Un estilo puede especificar atributos como el color y el tamaño de fuente, el color de fondo y mucho más, que se pueden aplicar a toda la app. En los codelabs posteriores, se aborda cómo implementarlos en tu app. Por el momento, ya lo hicimos para que tu app fuera más atractiva.

Para comprender mejor este concepto, se ofrece una comparación en paralelo de las versiones de la solución de la app con y sin un tema personalizado.

Sin un tema personalizado

Con un tema personalizado

La función de componibilidad TextField le permite al usuario ingresar texto en una app. Por ejemplo, observa el cuadro de texto que aparece en la pantalla de acceso de la app de Gmail que se muestra en esta imagen:

Pantalla de teléfono con la app de Gmail y un campo de texto para el correo electrónico

Agrega el elemento TextField componible a la app. Para ello, haz lo siguiente:

  1. En el archivo MainActivity.kt, agrega una función de componibilidad EditNumberField(), que toma un parámetro Modifier.
  2. En el cuerpo de la función EditNumberField(), debajo de TipTimeLayout(), agrega un TextField que acepte un parámetro con nombre value configurado como una cadena vacía y un parámetro con nombre onValueChange configurado como una expresión lambda vacía:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   TextField(
      value = "",
      onValueChange = {},
      modifier = modifier
   )
}
  1. Observa los parámetros que pasaste:
  • El parámetro value es un cuadro de texto que muestra el valor de cadena que pasas aquí.
  • El parámetro onValueChange es la devolución de llamada lambda que se activa cuando el usuario ingresa texto en el cuadro.
  1. Importa esta función:
import androidx.compose.material3.TextField
  1. En el elemento TipTimeLayout() componible, en la línea después de la primera función de componibilidad de texto, llama a la función EditNumberField() y pasa el siguiente modificador.
import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun TipTimeLayout() {
   Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
   ) {
       Text(
           ...
       )
       EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
       Text(
           ...
       )
       ...
   }
}

Se mostrará el cuadro de texto en la pantalla.

  1. En el panel Design, deberías ver el texto Calculate Tip, un cuadro de texto vacío y el texto Tip Amount componible.

2c208378cd4b8d41.png

4. Usa el estado en Compose

El estado de una app es cualquier valor que puede cambiar con el paso del tiempo. En esta app, el estado es el importe de la factura.

Agrega una variable al estado de almacenamiento:

  1. Al comienzo de la función EditNumberField(), usa la palabra clave val para agregar una variable amountInput establecida en el valor "0":
val amountInput = "0"

Es el estado de la app según el importe de la factura.

  1. Establece el parámetro llamado value en un valor amountInput:
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. Revisa la vista previa. El cuadro de texto muestra el valor establecido para la variable de estado, como se puede observar en esta imagen:

e8e24821adfd9d8c.png

  1. Ejecuta la app en el emulador e intenta ingresar un valor diferente. El estado codificado no se modifica porque el elemento TextField componible no se actualiza. Se actualiza cuando cambia su parámetro value, que se establece en la propiedad amountInput.

La variable amountInput representa el estado del cuadro de texto. Tener un estado codificado no es útil porque no se puede modificar y no refleja las entradas del usuario. Debes actualizar el estado de la app cuando el usuario actualice el importe de la factura.

5. La composición

Los elementos componibles de tu app describen una IU que muestra una columna con texto, un espaciador y un cuadro de texto. El texto muestra un texto Calculate Tip y el cuadro de texto muestra un valor 0 o el valor predeterminado.

Compose es un framework declarativo de IU, lo que significa que declaras cómo debería verse la IU en tu código. Si deseas que el cuadro de texto muestre un valor 100 inicialmente, debes establecer el valor inicial en el código de los elementos componibles en un valor 100.

¿Qué sucede si deseas que la IU cambie mientras se ejecuta la app o cuando el usuario interactúa con ella? Por ejemplo, ¿qué pasa si deseas actualizar la variable amountInput con el valor que ingresó el usuario y mostrarlo en el cuadro de texto? Es entonces cuando dependes de un proceso llamado recomposición para actualizar la composición de la app.

La composición es una descripción de la IU que crea Compose cuando ejecuta elementos componibles. Las apps de Compose llaman a funciones de componibilidad para transformar datos en IU. 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 programa una recomposición por ti.

Cuando Compose ejecute tus elementos componibles por primera vez, durante la composición inicial, mantendrá un registro de los elementos componibles a los que llamas para describir tu IU en un objeto Composition. Una recomposición se genera cuando Jetpack Compose vuelve a ejecutar los elementos componibles que pueden haberse modificado en respuesta a cambios de estado y, luego, actualiza la composición para reflejar los cambios.

Una composición solo puede producirse con una composición inicial y actualizarse a través de la recomposición. La única forma de modificar un objeto Composition es mediante la recomposición. Para ello, Compose necesita saber de qué estado se debe hacer el seguimiento para poder programar la recomposición cuando recibe una actualización. En tu caso, es la variable amountInput, por lo que, cuando cambia su valor, Compose programa una recomposición.

Puedes usar los tipos State y MutableState en Compose para que Compose pueda observar o hacer un seguimiento del estado de tu app. El tipo State es inmutable, por lo que solo puedes leer el valor que tiene, mientras que el tipo MutableState es mutable. 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.

El valor que muestra la función mutableStateOf():

  • Contiene el estado, que es el importe de la factura.
  • Es mutable, por lo que se puede cambiar el valor.
  • Como es observable, Compose observa cualquier cambio en el valor y activa una recomposición para actualizar la IU.

Agrega un estado de costo de servicio:

  1. En la función EditNumberField(), cambia la palabra clave val antes de la variable de estado amountInput por la palabra clave var:
var amountInput = "0"

Esto hace que amountInput sea mutable.

  1. Usa el tipo MutableState<String> en lugar de la variable String codificada para que Compose sepa que debe hacer un seguimiento del estado de amountInput y, luego, pase una cadena "0", que es el valor inicial predeterminado de la variable de estado amountInput:
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

var amountInput: MutableState<String> = mutableStateOf("0")

La inicialización de amountInput también se puede escribir de la siguiente manera con inferencia de tipo:

var amountInput = mutableStateOf("0")

La función mutableStateOf() recibe un valor inicial "0" como argumento, que luego hace que amountInput sea observable. Como resultado, se mostrará esta advertencia de compilación en Android Studio, pero pronto se corregirá:

Creating a state object during composition without using remember.
  1. En la función de componibilidad TextField, usa la propiedad amountInput.value:
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

Compose realiza un seguimiento de cada elemento componible que lee las propiedades value del estado y activa una recomposición cuando cambia su value.

La devolución de llamada onValueChange se activa cuando cambia la entrada del cuadro de texto. En la expresión lambda, la variable it contiene el valor nuevo.

  1. En la expresión lambda del parámetro con nombre onValueChange, configura la propiedad amountInput.value como la variable it:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
       modifier = modifier
   )
}

Estás actualizando el estado de TextField (es decir, la variable amountInput) cuando TextField te notifica que hay un cambio en el texto a través de la función de devolución de llamada onValueChange.

  1. Ejecuta la app y, luego, ingresa texto en el cuadro de texto. El cuadro de texto aún muestra un valor 0, como se observa en esta imagen:

3a2c62f8ec55e339.gif

Cuando el usuario ingresa texto en el cuadro, se llama a la devolución de llamada onValueChange y se actualiza la variable amountInput con el valor nuevo. Compose realiza un seguimiento del estado amountInput, por lo que, en el momento en que cambia su valor, se programa la recomposición y se vuelve a ejecutar la función de componibilidad EditNumberField(). En esa función de componibilidad, la variable amountInput se restablece a su valor 0 inicial. Por lo tanto, en el cuadro de texto, se muestra un valor 0.

Con el código que agregaste, los cambios de estado hacen que se programen las recomposiciones.

Sin embargo, necesitas una manera de preservar el valor de la variable amountInput entre las recomposiciones para que no se restablezca a un valor 0 cada vez que se recomponga la función EditNumberField(). Resolverás este problema en la siguiente sección.

6. Usa la función de recordatorio para guardar el estado

Gracias a la recomposición, es posible llamar a los métodos de composición varias veces. El elemento componible restablece su estado durante la recomposición si no se guarda.

Las funciones de componibilidad pueden almacenar un objeto entre recomposiciones con remember. Un valor calculado por la función remember se almacena en la composición durante la composición inicial, y el valor almacenado se muestra durante la recomposición. Por lo general, las funciones remember y mutableStateOf se usan juntas en funciones que admiten composición para que el estado y sus actualizaciones se reflejen de forma correcta en la IU.

Usa la función remember en la función EditNumberField():

  1. En la función EditNumberField(), inicializa la variable amountInput con el delegado de propiedad by remember de Kotlin, y rodea la llamada a la función mutableStateOf() con remember.
  2. En la función mutableStateOf(), pasa una cadena vacía en lugar de una cadena "0" estática:
var amountInput by remember { mutableStateOf("") }

Ahora la cadena vacía es el valor predeterminado inicial para la variable amountInput. by es una delegación de propiedades de Kotlin. Las funciones del método get y el método set de la propiedad amountInput se delegan a las funciones del método get y el método set de la clase remember, respectivamente.

  1. Importa estas funciones:
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Agregar las importaciones de métodos get y set del delegado te permite leer y configurar amountInput sin hacer referencia a la propiedad value del elemento MutableState.

La función EditNumberField() actualizada debería verse de la siguiente manera:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       modifier = modifier
   )
}
  1. Ejecuta la app e ingresa texto en el cuadro de texto. Deberías ver el texto que escribiste ahora.

59ac301a208b47c4.png

7. Estado y recomposición en acción

En esta sección, estableces un punto de interrupción y depuras la función de componibilidad EditNumberField() para ver cómo funcionan la composición y la recomposición iniciales.

Establece un punto de interrupción y depura la app en un emulador o dispositivo:

  1. En la función EditNumberField(), junto al parámetro con nombre onValueChange, establece un punto de interrupción de línea.
  2. En el menú de navegación, haz clic en Debug 'app'. La app se inicia en el emulador o dispositivo. La ejecución de tu app se detiene por primera vez cuando se crea el elemento TextField.

154e060231439307.png

  1. En el panel Debug, haz clic en 2a29a3bad712bec.png Resume Program. Se crea el cuadro de texto.
  2. En el emulador o dispositivo, ingresa una letra en el cuadro de texto. Se volverá a pausar la ejecución de tu app cuando alcance el punto de interrupción que configuraste.

Cuando ingresas el texto, se llama a la devolución de llamada onValueChange. Dentro de la lambda, it tiene el nuevo valor que escribiste en el teclado.

Una vez que el valor de "it" se asigna a amountInput, Compose activa la recomposición con los datos nuevos a medida que cambia el valor observable.

1d5e08d32052d02e.png

  1. En el panel Debug, haz clic en 2a29a3bad712bec.png Resume Program. El texto ingresado en el emulador o en el dispositivo se muestra junto a la línea con el punto de interrupción, como se observa en esta imagen:

1f5db6ab5ca5b477.png

Es el estado del campo de texto.

  1. Haz clic en 2a29a3bad712bec.png Resume Program. El valor ingresado se muestra en el emulador o dispositivo.

8. Modifica el aspecto

En la sección anterior, lograste que el campo de texto funcionara. En esta sección, mejorarás la IU.

Cómo agregar una etiqueta al cuadro de texto

Todos los cuadros de texto deben tener una etiqueta que les permita a los usuarios saber qué información pueden ingresar. En la primera parte de la siguiente imagen de ejemplo, el texto de la etiqueta se encuentra en el medio de un campo de texto y alineado con la línea de entrada. En la segunda parte de la siguiente imagen de ejemplo, la etiqueta se mueve más arriba en el cuadro de texto cuando el usuario hace clic en él para ingresar texto. Para obtener más información sobre la anatomía del campo de texto, consulta Anatomía.

a2afd6c7fc547b06.png

Modifica la función EditNumberField() para agregar una etiqueta al campo de texto:

  1. En la función de componibilidad TextField() de la función EditNumberField(), agrega un parámetro llamado label configurado como una expresión lambda vacía:
TextField(
//...
   label = { }
)
  1. En la expresión lambda, llama a la función Text() que acepte un stringResource(R.string.bill_amount):
label = { Text(stringResource(R.string.bill_amount)) },
  1. En la función de componibilidad TextField(), agrega el parámetro con nombre singleLine configurado en un valor true:
TextField(
  // ...
   singleLine = true,
)

Esto condensa el cuadro de texto en una sola línea desplazable horizontalmente a partir de varias líneas.

  1. Agrega el parámetro keyboardOptions configurado en una KeyboardOptions():
import androidx.compose.foundation.text.KeyboardOptions

TextField(
  // ...
   keyboardOptions = KeyboardOptions(),
)

Android ofrece una opción para configurar el teclado que se muestra en la pantalla para ingresar dígitos, direcciones de correo electrónico, URL y contraseñas, entre otros. Para obtener más información, consulta KeyboardType.

  1. Fija el tipo de teclado en número para ingresar dígitos. Pasa la función KeyboardOptions a un parámetro con nombre keyboardType configurado en un KeyboardType.Number:
import androidx.compose.ui.text.input.KeyboardType

TextField(
  // ...
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)

La función EditNumberField() completa debería verse como este fragmento de código:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
    var amountInput by remember { mutableStateOf("") }
    TextField(
        value = amountInput,
        onValueChange = { amountInput = it },
        singleLine = true,
        label = { Text(stringResource(R.string.bill_amount)) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        modifier = modifier
    )
}
  1. Ejecuta la app.

En esta captura de pantalla, puedes ver los cambios realizados en el teclado:

55936268bf007ee9.png

9. Muestra el importe de la propina

En esta sección, implementarás la funcionalidad principal de la app, que es la capacidad de calcular y mostrar el importe de la propina.

En el archivo MainActivity.kt, se te proporciona una función private calculateTip() como parte del código de partida. Usarás esta función para calcular el importe de la propina:

private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
    val tip = tipPercent / 100 * amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

En el método anterior, usas NumberFormat para mostrar el formato de la propina como moneda.

Ahora, tu app puede calcular la propina, pero aún debes darle formato y mostrarla con la clase.

Usa la función calculateTip()

El texto ingresado por el usuario en el campo de texto componible se muestra a la función de devolución de llamada onValueChange como String, aunque el usuario haya ingresado un número. Para solucionar este problema, debes convertir el valor amountInput, que contiene el importe que ingresó el usuario.

  1. En la función de componibilidad EditNumberField(), crea una variable nueva llamada amount después de la definición de amountInput. Llama a la función toDoubleOrNull en la variable amountInput para convertir String en Double:
val amount = amountInput.toDoubleOrNull()

toDoubleOrNull() es una función de Kotlin predefinida que analiza una cadena como un número Double y muestra el resultado o null si la cadena no es una representación válida de un número.

  1. Al final de la sentencia, agrega un operador Elvis ?: que muestra un valor 0.0 cuando amountInput sea nulo:
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. Después de la variable amount, crea otra variable val llamada tip. Debes inicializarla con calculateTip() y pasar el parámetro amount.
val tip = calculateTip(amount)

La función EditNumberField() completa debería verse como este fragmento de código:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.bill_amount)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

Muestra el importe calculado de la propina

Escribiste la función para calcular el importe de la propina. El siguiente paso es mostrar el importe calculado de la propina:

  1. En la función TipTimeLayout() al final del bloque Column(), observa el texto componible que muestra $0.00. Actualizarás este valor al importe calculado de la propina.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // ...
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        // ...
    }
}

Debes acceder a la variable amountInput en la función TipTimeLayout() para calcular y mostrar el importe de la propina, pero la variable amountInput es el estado del campo de texto definido en la función de componibilidad EditNumberField(). Aún no puedes llamarlo desde la función TipTimeLayout(). En esta imagen, se muestra la estructura del código:

50bf0b9d18ede6be.png

Esta estructura no te permitirá mostrar el importe de la propina en el nuevo elemento Text componible porque el elemento Text debe acceder a la variable amount calculada desde la variable amountInput. Debes exponer la variable amount a la función TipTimeLayout(). En esta imagen, se muestra la estructura de código deseada, lo que hace que el elemento EditNumberField() componible no tenga estado:

ab4ec72388149f7c.png

Este patrón se conoce como elevación de estado. En la siguiente sección, elevas el estado desde un elemento componible para que no tenga estado.

10. Elevación de estado

En esta sección, aprenderás a decidir dónde definir tu estado de manera que puedas volver a usar y compartir tus elementos componibles.

En una función de componibilidad, puedes definir variables que muestren el estado de la IU. Por ejemplo, definiste la variable amountInput como estado en el elemento EditNumberField() componible.

Cuando tu app se vuelva más compleja y otros elementos componibles necesiten acceder al estado dentro del elemento EditNumberField() componible, deberás considerar la elevación o extracción del estado fuera de la función de componibilidad EditNumberField().

Cómo interpretar los elementos que admiten composición con estado y sin estado

Debes elevar el estado cuando necesites hacer lo siguiente:

  • Compartir el estado con varias funciones de componibilidad
  • Crear un elemento sin estado componible que se pueda volver a usar en tu app

Cuando extraes el estado de una función de componibilidad, la función de componibilidad resultante se considera sin estado. Es decir, las funciones de componibilidad pueden dejar de tener estado si se lo extrae de ellas.

Un elemento sin estado componible significa que no contiene, define ni modifica un estado nuevo. Por otro lado, un elemento con estado componible es aquel que posee una parte de estado que puede cambiar con el tiempo.

La elevación de estado es un patrón que consiste en mover el estado hacia el llamador para hacer que el componente no tenga estado.

Cuando se aplica a los elementos componibles, esto suele implicar incorporar dos parámetros a este elemento:

  • Un parámetro value: T, que es el valor actual que se mostrará.
  • Una lambda de devolución de llamada onValueChange: (T) -> Unit, que se activa cuando cambia el valor para que el estado se pueda actualizar en otro lugar, como cuando un usuario ingresa texto en el cuadro de texto.

Eleva el estado en la función EditNumberField():

  1. Actualiza la definición de la función EditNumberField() para elevar el estado agregando los parámetros value y onValueChange.
@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
//...

El parámetro value es de tipo String y el parámetro onValueChange es de tipo (String) -> Unit, por lo que es una función que toma un valor String como entrada y no tiene valor de retorno. El parámetro onValueChange se usa como la devolución de llamada onValueChange que se pasa al elemento TextField componible.

  1. En la función EditNumberField(), actualiza la función de componibilidad TextField() para usar los parámetros que se pasaron:
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. Eleva el estado, mueve el estado recordado de la función EditNumberField() a la función TipTimeLayout():
@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)
  
   Column(
       //...
   ) {
       //...
   }
}
  1. Elevaste el estado a TipTimeLayout() y, ahora, lo pasas a EditNumberField(). En la función TipTimeLayout(), actualiza la llamada a la función EditNumberField() para usar el estado elevado:
EditNumberField(
   value = amountInput,
   onValueChange = { amountInput = it },
   modifier = Modifier
       .padding(bottom = 32.dp)
       .fillMaxWidth()
)

Esto hace que EditNumberField no tenga estado. Elevaste el estado de la IU a su principal, TipTimeLayout(). Ahora, TipTimeLayout() es el propietario del estado (amountInput).

Formato posicional

El formato posicional se usa para mostrar contenido dinámico en cadenas. Por ejemplo, imagina que deseas que el cuadro de texto Importe de la propina muestre un valor xx.xx que podría ser cualquier importe calculado y con formato en tu función. Para lograr esto en el archivo strings.xml, debes definir el recurso de cadenas con un argumento de marcador de posición, como este fragmento de código:

// No need to copy.

// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>

En el código de redacción, puedes tener varios argumentos de marcador de posición de cualquier tipo. Un marcador de posición string es %s.

Observa el elemento de texto componible en TipTimeLayout() y pasa la propina con formato como un argumento a la función stringResource().

// No need to copy
Text(
   text = stringResource(R.string.tip_amount, "$0.00"),
   style = MaterialTheme.typography.displaySmall
)
  1. En la función, TipTimeLayout(), usa la propiedad tip para mostrar el importe de la propina. Actualiza el parámetro text del elemento Text componible para usar la variable tip como parámetro.
Text(
     text = stringResource(R.string.tip_amount, tip),
     // ...

Las funciones TipTimeLayout() y EditNumberField() completadas deberían verse como este fragmento de código:

@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }
   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
       horizontalAlignment = Alignment.CenterHorizontally,
       verticalArrangement = Arrangement.Center
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           modifier = Modifier
               .padding(bottom = 16.dp, top = 40.dp)
               .align(alignment = Alignment.Start)
       )
       EditNumberField(
           value = amountInput,
           onValueChange = { amountInput = it },
           modifier = Modifier
               .padding(bottom = 32.dp)
               .fillMaxWidth()
       )
       Text(
           text = stringResource(R.string.tip_amount, tip),
           style = MaterialTheme.typography.displaySmall
       )
       Spacer(modifier = Modifier.height(150.dp))
   }
}

@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   TextField(
       value = value,
       onValueChange = onValueChange,
       singleLine = true,
       label = { Text(stringResource(R.string.bill_amount)) },
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
       modifier = modifier
   )
}

En resumen, elevaste el estado amountInput del EditNumberField() al elemento TipTimeLayout() componible. Para que el cuadro de texto funcione como antes, debes pasar dos argumentos a la función de componibilidad EditNumberField(): el valor amountInput y la devolución de llamada lambda que actualiza el valor amountInput desde la entrada del usuario. Estos cambios te permiten calcular la propina a partir de la propiedad amountInput en TipTimeLayout() para mostrarla al usuario.

  1. Ejecuta la app en el emulador o dispositivo, y, luego, ingresa un valor en el cuadro de texto de importe de la factura. El importe de la propina del 15% del importe de la factura se muestra como se ve en esta imagen:

de593783dc813e24.png

11. Obtén el código de solución

Para descargar el código del codelab terminado, puedes usar estos comandos de git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout state

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución, puedes hacerlo en GitHub.

12. Conclusión

¡Felicitaciones! Completaste este codelab y aprendiste a usar el estado en una app de Compose.

Resumen

  • El estado de una app es cualquier valor que puede cambiar con el paso del tiempo.
  • La composición es una descripción de la IU que crea Compose cuando ejecuta elementos componibles. Las apps de Compose llaman a funciones de componibilidad para transformar datos en la IU.
  • La composición inicial es una creación de la IU por parte de Compose cuando ejecuta funciones componibles por primera vez.
  • La recomposición es el proceso de volver a ejecutar los mismos elementos componibles para actualizar el árbol cuando cambian sus datos.
  • La elevación de estado es un patrón que consiste en mover el estado hacia el llamador para hacer que el componente no tenga estado.

Más información