Introducción al estado en Compose

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

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

761df483de663721.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 que admiten composición
  • 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
  • Android Studio

2. Mira el video con instrucciones para compilar (opcional)

Si quieres ver cómo uno de los instructores del curso completa el codelab, reproduce el siguiente video.

Se recomienda expandir el video a pantalla completa (con el ícono Este símbolo muestra 4 esquinas en un cuadrado destacado para indicar el modo de pantalla completa. en la esquina inferior derecha del video) para que puedas ver Android Studio y el código con mayor claridad.

Este paso es opcional. También puedes omitir el video y comenzar con las instrucciones del codelab de inmediato.

3. Cómo comenzar

  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 Facturación y 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 costo de servicio, la app mostrará un importe sugerido de la propina. Por el momento, el porcentaje de propina está establecido 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.

aaf86be8d13431f5.png

761df483de663721.png

4. Cómo crear un proyecto

Configura un proyecto en Android Studio con la plantilla de actividad de Compose vacía y los recursos de strings requeridos:

  1. En Android Studio, crea un proyecto con la plantilla de actividad vacía de Compose, ingresa Tip Time como nombre y selecciona API 21: Android 5.0 (Lollipop) o una versión posterior como el SDK mínimo. Se cargan los archivos del proyecto.
  2. En el panel Project, haz clic en res > values > strings.xml. Debes tener un solo recurso de strings para el nombre de la app.
  3. Entre las etiquetas <resources>, ingresa estos recursos de strings:
<string name="calculate_tip">Calculate Tip</string>
<string name="cost_of_service">Cost of Service</string>
<string name="tip_amount">Tip amount: %s</string>

El archivo strings.xml debería verse como este fragmento de código:

strings.xml

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="cost_of_service">Cost of Service</string>
   <string name="tip_amount">Tip amount: %s</string>
</resources>

5. Cómo agregar un título de la pantalla

En esta sección, agregarás un título de la pantalla a la app con la función de componibilidad Text.

Borra la función Greeting() y agrega una función TipTimeScreen() a fin de agregar los elementos de la IU necesarios para la app:

  1. En el archivo MainActivity.kt, borra la función Greeting().
// Delete this.
@Composable
fun Greeting(name: String) {
   //...
}
  1. En las funciones onCreate() y DefaultPreview(), borra las llamadas a la función Greeting():
// Delete this.
Greeting("Android")
  1. Debajo de la función onCreate(), agrega una función de componibilidad TipTimeScreen() para representar la pantalla de la app:
@Composable
fun TipTimeScreen() {
}
  1. En el bloque Surface() de la función onCreate(), llama a la función TipTimeScreen():
override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeScreen()
           }
       }
   }
}
  1. En el bloque TipTimeTheme de la función DefaultPreview(), llama a la función TipTimeScreen():
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
   TipTimeTheme {
       TipTimeScreen()
   }
}

Cómo mostrar el título de la pantalla

Implementa la función TipTimeScreen() para mostrar el título de la pantalla:

  1. En la función TipTimeScreen(), agrega un elemento Column. Los elementos se encuentran en una columna vertical, por lo que usas un elemento Column.
  2. En el bloque Column, pasa un parámetro llamado modifier configurado como una función Modifier.padding que acepte un argumento 32.dp:
Column(
   modifier = Modifier.padding(32.dp)
) {}
  1. Importa estas funciones y esta propiedad:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
  1. En la función Column, pasa un argumento llamado verticalArrangement configurado como una función Arrangement.spacedBy que acepte un argumento 8.dp:
Column(
   modifier = Modifier.padding(32.dp),
   verticalArrangement = Arrangement.spacedBy(8.dp)
) {}

Se agregará un espacio de 8dp fijos entre elementos secundarios.

  1. Importa lo siguiente:
import androidx.compose.foundation.layout.Arrangement
  1. Agrega un elemento Text que tome un parámetro con nombre text establecido como una función stringResource(R.string.calculate_tip), un parámetro con nombre fontSize establecido como un valor 24.sp y un argumento con nombre modifier configurado como una función Modifier.align(Alignment.CenterHorizontally):
Text(
   text = stringResource(R.string.calculate_tip),
   fontSize = 24.sp,
   modifier = Modifier.align(Alignment.CenterHorizontally)
)
  1. Importa estas importaciones:
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Alignment
  1. En el panel Design, haz clic en Build & Refresh. Deberías ver Calculate Tip como título de la pantalla, que es el elemento de texto que agregaste.

da56236494529e77.png

Agrega el elemento TextField que admite composición

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

58671affa01fb9e1.png

La función de componibilidad TextField 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:

30d9c9123b5d26fe.png

Agrega el elemento TextField que admite composición a la app:

  1. En el bloque Column después del elemento Text, agrega una función de componibilidad Spacer() con una altura de 16dp.
@Composable
fun TipTimeScreen() {
   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           ...
       )
       Spacer(Modifier.height(16.dp))
   }
}

Se mostrará un espacio de 16dp vacío después del título de la pantalla.

  1. Importa estas funciones:
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
  1. En el archivo MainActivity.kt, agrega una función de componibilidad EditNumberField().
  2. En el cuerpo de la función EditNumberField(), agrega un TextField que acepte un parámetro con nombre value configurado como una string vacía y un parámetro con nombre onValueChange configurado como una expresión lambda vacía:
@Composable
fun EditNumberField() {
   TextField(
      value = "",
      onValueChange = {},
   )
}
  1. Observa los parámetros que pasaste:
  • El parámetro value es un cuadro de texto que muestra el valor de string 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.material.TextField
  1. En la línea después de la función de componibilidad Spacer(), llama a la función EditNumberField():
@Composable
fun TipTimeScreen() {
   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           ...
       )
       Spacer(Modifier.height(16.dp))
       EditNumberField()
   }
}

Se mostrará el cuadro de texto en la pantalla.

  1. En el panel Design, haz clic en be24da86724b252c.png Build & Refresh. Deberías ver el título de pantalla Calculate Tip y un cuadro de texto vacío con un espacio 16dp entre ellos.

1ff60ec32d3b15c1.png

6. Cómo usar 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 costo del servicio.

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 asignada a un valor "0" estático:
val amountInput = "0"

Este es el estado de la app en función del costo del servicio.

  1. Establece el parámetro con nombre value en un valor amountInput:
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. Vuelve a compilar y ejecutar la app. El cuadro de texto muestra el valor establecido para la variable de estado, como se puede observar en esta imagen:

ba0f07ef1162855b.png

  1. Ingresa 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 actualiza el costo del servicio.

7. La composición

Los elementos integrables de tu app describen una IU que muestra una columna con texto, un espaciador y un cuadro de texto. El texto muestra el título Calculate tip, el espaciador Spacer tiene una altura de 16dp y el cuadro de texto muestra un valor de 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 que admiten composición 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 que admiten composición. 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 que admiten composición 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 que admiten composición por primera vez, durante la composición inicial, mantendrá un registro de los elementos que admiten composición 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 mediante una composición inicial y actualizarse mediante 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 a fin de 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 costo del servicio.
  • 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 sea mutable.

  1. Utiliza 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 un string "0", que es el valor inicial predeterminado de la variable de estado amountInput:
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 "0" inicial como un parámetro que está unido a un objeto State, lo que luego hace que su value 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 = { },
)

Compose realiza un seguimiento de cada elemento que admite composición 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() {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
   )
}

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:

6cb691703cc7ecbf.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.

8. Cómo usar 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 que admite composición restablece su estado durante la recomposición si no se guarda.

Las funciones que admiten composición 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 string vacía en lugar de una string "0" estática:
var amountInput by remember { mutableStateOf("") }

Ahora la string 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.mutableStateOf
import androidx.compose.runtime.remember
  1. Importa estas funciones manualmente:
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() {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
   )
}
  1. Ejecuta la app e ingresa texto en el cuadro de texto. Ahora, deberías ver el texto que ingresaste.

270943a84f18572d.png

9. 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 la app se detiene por primera vez cuando se crea el elemento TextField.

e225f2d67e9f2c40.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, Compose activa una recomposición, y se llama a la devolución de llamada onValueChange en la función EditNumberField() con los datos nuevos, como se observa en esta imagen:

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 que ingresaste se muestra en el emulador o dispositivo.

10. Cómo modificar 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.cost_of_service):
label = { Text(stringResource(R.string.cost_of_service)) }
  1. En la función de componibilidad TextField(), agrega un parámetro llamado modifier configurado como Modifier.fillMaxWidth():
TextField(
  // Other parameters
   modifier = Modifier.fillMaxWidth(),
)
  1. Importa lo siguiente:
import androidx.compose.foundation.layout.fillMaxWidth
  1. En la función de componibilidad TextField(), agrega el parámetro con nombre singleLine configurado en un valor true:
TextField(
  // Other parameters
   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():
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions()
)

Android ofrece una opción para configurar el teclado que se muestra en la pantalla a fin de 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 a fin de ingresar dígitos. Pasa la función KeyboardOptions a un parámetro con nombre keyboardType configurado en un KeyboardType.Number:
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)

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

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

   )
}
  1. Importa lo siguiente:
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
  1. Ejecuta la app.

Puedes ver los cambios en esta imagen:

48368bf5df67af37.png

11. Cómo mostrar 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.

Al finalizar esta tarea, tu app tendrá el siguiente aspecto:

aaf86be8d13431f5.png

Cómo calcular el importe de la propina

Define e implementa una función que acepte el costo de servicio y el porcentaje de propina, y que muestre el importe de la propina:

  1. En el archivo MainActivity.kt después de la función EditNumberField(), agrega una función private calculateTip().
  2. Agrega los parámetros con nombre amount y tipPercent, ambos de tipo Double. El parámetro amount pasa el costo del servicio.
  3. Establece el parámetro tipPercent 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:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
}
  1. En el cuerpo de la función, usa la palabra clave val para definir una variable tip que divide el parámetro tipPercent por un valor 100 y multiplica el resultado por el parámetro amount para calcular la propina:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
   val tip = tipPercent / 100 * amount
}

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

  1. En la siguiente línea del cuerpo de la función calculateTip(), llama a la función NumberFormat.getCurrencyInstance():
NumberFormat.getCurrencyInstance()

De esta manera, obtendrás un formateador de números que puedes usar para darles el formato de monedas a los números.

  1. En la llamada a función NumberFormat.getCurrencyInstance(), encadena el método format() y pásalo a la variable tip como un parámetro.
NumberFormat.getCurrencyInstance().format(tip)
  1. Cuando Android Studio te lo solicite, importa esta clase.
import java.text.NumberFormat
  1. El último paso es mostrar la string con formato de la función. Modifica la firma de la función para mostrar un tipo String. Agrega la palabra clave return delante de la sentencia NumberFormat:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

Ahora, la función muestra una string con formato.

Cómo usar la función calculateTip().

El texto ingresado por el usuario en el campo de texto que admite composición 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(), 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 string como un número Double y muestra el resultado, o null si la string 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() {
   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.cost_of_service)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

Cómo mostrar el importe calculado de la propina

Escribiste la función para calcular el importe de la propina. El siguiente paso es agregar un elemento Text que admite composición para mostrar el importe calculado de la propina:

97734d91a3844d22.png

  1. En la función TipTimeScreen() al final del bloque Column(), agrega un elemento Spacer() que admite composición y pasa una altura de 24.dp:
@Composable
fun TipTimeScreen() {
   Column(
       //...
   ) {
       Text(
           //...
       )
       //...
       EditNumberField()
       Spacer(Modifier.height(24.dp))
   }
}

De esta manera, se agrega un espacio después del campo de texto.

  1. Después del elemento Spacer() componible, agrega el siguiente elemento Text también componible:
Text(
   text = stringResource(R.string.tip_amount, ""),
   modifier = Modifier.align(Alignment.CenterHorizontally),
   fontSize = 20.sp,
   fontWeight = FontWeight.Bold
)

Este código usa el recurso de strings tip_amount para definir el texto, pero no se muestra el importe de la propina. Solucionar este problema es muy sencillo. Centra el texto en la pantalla en un tamaño de 20.sp con el tamaño de fuente establecido en negrita.

  1. Importa las siguientes importaciones:
import androidx.compose.ui.text.font.FontWeight

Debes acceder a la variable amountInput en la función TipTimeScreen 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 TipTimeScreen(). En esta imagen, se muestra la estructura del código:

5ec86acdbfa1907b.png

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

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

12. 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 que admitan composición necesiten acceder al estado dentro del elemento EditNumberField() que admite composición, 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 no tiene estado, lo que significa que no tiene, define ni modifica un nuevo estado. 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 en el que el estado se mueve a una función diferente a fin de dejar a un componente sin 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() a fin de elevar el estado agregando los parámetros value y onValueChange.
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit
)

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 que admite composición.

  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 TipTimeScreen():
@Composable
fun TipTimeScreen() {
   var amountInput by remember { mutableStateOf("") }

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

   Column(
       //...
   ) {
       //...
   }
}
  1. Elevaste el estado a TipTimeScreen() y, ahora, lo pasas a EditNumberField(). En la función TipTimeScreen(), actualiza la llamada a la función EditNumberField() para usar el estado elevado:
EditNumberField(value = amountInput,
   onValueChange = { amountInput = it }
)
  1. Usa la propiedad tip para mostrar el importe de la propina. Actualiza el parámetro text del elemento que admite composición Text para usar la variable tip como parámetro. Esto se denomina formato posicional.
Text(
   text = stringResource(R.string.tip_amount, tip),
   // Rest of the code
)

Con el formato posicional, puedes mostrar contenido dinámico en strings. 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 strings 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>

// In your Compose code
Text(
    text = stringResource(R.string.tip_amount, tip)
)

Puedes tener varios marcadores de posición, y de cualquier tipo. Un marcador de posición string es %s. En Compose, debes pasar la propina con formato como un argumento a la función stringResource().

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

@Composable
fun TipTimeScreen() {
   var amountInput by remember { mutableStateOf("") }

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

   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           fontSize = 24.sp,
           modifier = Modifier.align(Alignment.CenterHorizontally)
       )
       Spacer(Modifier.height(16.dp))
       EditNumberField(value = amountInput,
           onValueChange = { amountInput = it }
       )
       Spacer(Modifier.height(24.dp))
       Text(
           text = stringResource(R.string.tip_amount, tip),
           modifier = Modifier.align(Alignment.CenterHorizontally),
           fontSize = 20.sp,
           fontWeight = FontWeight.Bold
       )
   }

}

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

En resumen, elevaste el estado amountInput del EditNumberField() al elemento que admite composición TipTimeScreen(). 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 TipTimeScreen() a fin de mostrarla al usuario.

  1. Ejecuta la app en el emulador o dispositivo y, luego, ingresa un valor en el cuadro de texto Cost of Service. Se mostrará la propina del 15%, como se observa en esta imagen:

13. Cómo obtener el código de la solución

Para descargar el código del codelab terminado, puedes usar este comando 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.

14. 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 que admiten composición. Las apps de Compose llaman a funciones de componibilidad para transformar datos en IU.
  • La composición inicial es una creación de la IU por parte de Compose cuando ejecuta funciones que admiten composición por primera vez.
  • La recomposición es el proceso de volver a ejecutar los mismos elementos que admiten composición a fin de actualizar el árbol cuando cambian sus datos.
  • La elevación de estado es el patrón en el que el estado se mueve hacia arriba a fin de dejar a un componente sin estado.

Más información