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:
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
yColumn
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
- 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.
- Ingresa valores diferentes en los cuadros Bill (facturación) y Tip % (porcentaje de propina). El valor total y de la propina cambian.
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í:
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:
- Abre el proyecto con el código de partida en Android Studio.
- Ejecuta la app en un dispositivo Android o en un emulador.
- Verás dos componentes de texto: uno es para una etiqueta y el otro es para mostrar el importe de la propina.
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 elementoColumn
con dos elementos de texto componibles y que ves en las capturas de pantalla. También tiene un elementospacer
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ámetrotipPercent
se establece en un valor de argumento predeterminado15.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()
}
}
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:
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:
Agrega el elemento TextField
componible a la app. Para ello, haz lo siguiente:
- En el archivo
MainActivity.kt
, agrega una función de componibilidadEditNumberField()
, que toma un parámetroModifier
. - En el cuerpo de la función
EditNumberField()
, debajo deTipTimeLayout()
, agrega unTextField
que acepte un parámetro con nombrevalue
configurado como una cadena vacía y un parámetro con nombreonValueChange
configurado como una expresión lambda vacía:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
- 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.
- Importa esta función:
import androidx.compose.material3.TextField
- En el elemento
TipTimeLayout()
componible, en la línea después de la primera función de componibilidad de texto, llama a la funciónEditNumberField()
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.
- En el panel Design, deberías ver el texto
Calculate Tip
, un cuadro de texto vacío y el textoTip Amount
componible.
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:
- Al comienzo de la función
EditNumberField()
, usa la palabra claveval
para agregar una variableamountInput
establecida en el valor"0"
:
val amountInput = "0"
Es el estado de la app según el importe de la factura.
- Establece el parámetro llamado
value
en un valoramountInput
:
TextField(
value = amountInput,
onValueChange = {},
)
- Revisa la vista previa. El cuadro de texto muestra el valor establecido para la variable de estado, como se puede observar en esta imagen:
- 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ámetrovalue
, que se establece en la propiedadamountInput
.
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:
- En la función
EditNumberField()
, cambia la palabra claveval
antes de la variable de estadoamountInput
por la palabra clavevar
:
var amountInput = "0"
Esto hace que amountInput
sea mutable.
- Usa el tipo
MutableState<String>
en lugar de la variableString
codificada para que Compose sepa que debe hacer un seguimiento del estado deamountInput
y, luego, pase una cadena"0"
, que es el valor inicial predeterminado de la variable de estadoamountInput
:
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.
- En la función de componibilidad
TextField
, usa la propiedadamountInput.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.
- En la expresión lambda del parámetro con nombre
onValueChange
, configura la propiedadamountInput.value
como la variableit
:
@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
.
- 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:
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()
:
- En la función
EditNumberField()
, inicializa la variableamountInput
con el delegado de propiedadby
remember
de Kotlin, y rodea la llamada a la funciónmutableStateOf
()
conremember
. - 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.
- 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
)
}
- Ejecuta la app e ingresa texto en el cuadro de texto. Deberías ver el texto que escribiste ahora.
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:
- En la función
EditNumberField()
, junto al parámetro con nombreonValueChange
, establece un punto de interrupción de línea. - 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
.
- En el panel Debug, haz clic en Resume Program. Se crea el cuadro de texto.
- 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.
- En el panel Debug, haz clic en 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:
Es el estado del campo de texto.
- Haz clic en 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.
Modifica la función EditNumberField()
para agregar una etiqueta al campo de texto:
- En la función de componibilidad
TextField()
de la funciónEditNumberField()
, agrega un parámetro llamadolabel
configurado como una expresión lambda vacía:
TextField(
//...
label = { }
)
- En la expresión lambda, llama a la función
Text()
que acepte unstringResource
(R.string.
bill_amount
)
:
label = { Text(stringResource(R.string.bill_amount)) },
- En la función de componibilidad
TextField()
, agrega el parámetro con nombresingleLine
configurado en un valortrue
:
TextField(
// ...
singleLine = true,
)
Esto condensa el cuadro de texto en una sola línea desplazable horizontalmente a partir de varias líneas.
- Agrega el parámetro
keyboardOptions
configurado en unaKeyboardOptions()
:
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.
- Fija el tipo de teclado en número para ingresar dígitos. Pasa la función
KeyboardOptions
a un parámetro con nombrekeyboardType
configurado en unKeyboardType.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
)
}
- Ejecuta la app.
En esta captura de pantalla, puedes ver los cambios realizados en el teclado:
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.
- En la función de componibilidad
EditNumberField()
, crea una variable nueva llamadaamount
después de la definición deamountInput
. Llama a la funcióntoDoubleOrNull
en la variableamountInput
para convertirString
enDouble
:
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.
- Al final de la sentencia, agrega un operador Elvis
?:
que muestra un valor0.0
cuandoamountInput
sea nulo:
val amount = amountInput.toDoubleOrNull() ?: 0.0
- Después de la variable
amount
, crea otra variableval
llamadatip
. Debes inicializarla concalculateTip()
y pasar el parámetroamount
.
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:
- En la función
TipTimeLayout()
al final del bloqueColumn()
, 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:
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:
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()
:
- Actualiza la definición de la función
EditNumberField()
para elevar el estado agregando los parámetrosvalue
yonValueChange
.
@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.
- En la función
EditNumberField()
, actualiza la función de componibilidadTextField()
para usar los parámetros que se pasaron:
TextField(
value = value,
onValueChange = onValueChange,
// Rest of the code
)
- Eleva el estado, mueve el estado recordado de la función
EditNumberField()
a la funciónTipTimeLayout()
:
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
//...
) {
//...
}
}
- Elevaste el estado a
TipTimeLayout()
y, ahora, lo pasas aEditNumberField()
. En la funciónTipTimeLayout()
, actualiza la llamada a la funciónEditNumberField
()
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
)
- En la función,
TipTimeLayout()
, usa la propiedadtip
para mostrar el importe de la propina. Actualiza el parámetrotext
del elementoText
componible para usar la variabletip
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.
- 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:
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.
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.