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:
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
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 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
- 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 Facturación y 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 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.
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:
- 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. - En el panel Project, haz clic en res > values > strings.xml. Debes tener un solo recurso de strings para el nombre de la app.
- 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:
- En el archivo
MainActivity.kt
, borra la funciónGreeting()
.
// Delete this.
@Composable
fun Greeting(name: String) {
//...
}
- En las funciones
onCreate()
yDefaultPreview()
, borra las llamadas a la funciónGreeting()
:
// Delete this.
Greeting("Android")
- Debajo de la función
onCreate()
, agrega una función de componibilidadTipTimeScreen()
para representar la pantalla de la app:
@Composable
fun TipTimeScreen() {
}
- En el bloque
Surface()
de la funciónonCreate()
, llama a la funciónTipTimeScreen()
:
override fun onCreate(savedInstanceState: Bundle?) {
//...
setContent {
TipTimeTheme {
Surface(
//...
) {
TipTimeScreen()
}
}
}
}
- En el bloque
TipTimeTheme
de la funciónDefaultPreview()
, llama a la funciónTipTimeScreen()
:
@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:
- En la función
TipTimeScreen()
, agrega un elementoColumn
. Los elementos se encuentran en una columna vertical, por lo que usas un elementoColumn
. - En el bloque
Column
, pasa un parámetro llamadomodifier
configurado como una funciónModifier.padding
que acepte un argumento32.dp
:
Column(
modifier = Modifier.padding(32.dp)
) {}
- Importa estas funciones y esta propiedad:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
- En la función
Column
, pasa un argumento llamadoverticalArrangement
configurado como una funciónArrangement.spacedBy
que acepte un argumento8.dp
:
Column(
modifier = Modifier.padding(32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {}
Se agregará un espacio de 8dp
fijos entre elementos secundarios.
- Importa lo siguiente:
import androidx.compose.foundation.layout.Arrangement
- Agrega un elemento
Text
que tome un parámetro con nombretext
establecido como una funciónstringResource(R.string.
calculate_tip
)
, un parámetro con nombrefontSize
establecido como un valor24.sp
y un argumento con nombremodifier
configurado como una funciónModifier.align(Alignment.CenterHorizontally)
:
Text(
text = stringResource(R.string.calculate_tip),
fontSize = 24.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
- Importa estas importaciones:
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Alignment
- 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.
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:
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:
Agrega el elemento TextField
que admite composición a la app:
- En el bloque
Column
después del elementoText
, agrega una función de componibilidadSpacer()
con una altura de16dp
.
@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.
- Importa estas funciones:
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
- En el archivo
MainActivity.kt
, agrega una función de componibilidadEditNumberField()
. - En el cuerpo de la función
EditNumberField()
, agrega unTextField
que acepte un parámetro con nombrevalue
configurado como una string vacía y un parámetro con nombreonValueChange
configurado como una expresión lambda vacía:
@Composable
fun EditNumberField() {
TextField(
value = "",
onValueChange = {},
)
}
- 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.
- Importa esta función:
import androidx.compose.material.TextField
- En la línea después de la función de componibilidad
Spacer()
, llama a la funciónEditNumberField()
:
@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.
- En el panel Design, haz clic en
Build & Refresh. Deberías ver el título de pantalla
Calculate Tip
y un cuadro de texto vacío con un espacio16dp
entre ellos.
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:
- Al comienzo de la función
EditNumberField()
, usa la palabra claveval
para agregar una variableamountInput
asignada a un valor"0"
estático:
val amountInput = "0"
Este es el estado de la app en función del costo del servicio.
- Establece el parámetro con nombre
value
en un valoramountInput
:
TextField(
value = amountInput,
onValueChange = {},
)
- 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:
- Ingresa 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 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:
- 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 sea mutable.
- Utiliza 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 un string"0"
, que es el valor inicial predeterminado de la variable de estadoamountInput
:
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.
- En la función de componibilidad
TextField
, usa la propiedadamountInput.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.
- En la expresión lambda del parámetro con nombre
onValueChange
, configura la propiedadamountInput.value
como la variableit
:
@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
.
- 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.
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()
:
- 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 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.
- Importa estas funciones:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
- 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 },
)
}
- Ejecuta la app e ingresa texto en el cuadro de texto. Ahora, deberías ver el texto que ingresaste.
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:
- 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 la 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, 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:
- 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 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.
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.
cost_of_service
)
:
label = { Text(stringResource(R.string.cost_of_service)) }
- En la función de componibilidad
TextField()
, agrega un parámetro llamadomodifier
configurado comoModifier.
fillMaxWidth
()
:
TextField(
// Other parameters
modifier = Modifier.fillMaxWidth(),
)
- Importa lo siguiente:
import androidx.compose.foundation.layout.fillMaxWidth
- En la función de componibilidad
TextField()
, agrega el parámetro con nombresingleLine
configurado en un valortrue
:
TextField(
// Other parameters
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()
:
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.
- Fija el tipo de teclado en número a fin de ingresar dígitos. Pasa la función
KeyboardOptions
a un parámetro con nombrekeyboardType
configurado en unKeyboardType.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)
)
}
- Importa lo siguiente:
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
- Ejecuta la app.
Puedes ver los cambios en esta imagen:
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:
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:
- En el archivo
MainActivity.kt
después de la funciónEditNumberField()
, agrega una funciónprivate
calculateTip()
. - Agrega los parámetros con nombre
amount
ytipPercent
, ambos de tipoDouble
. El parámetroamount
pasa el costo del servicio. - Establece el parámetro
tipPercent
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:
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0
) {
}
- En el cuerpo de la función, usa la palabra clave
val
para definir una variabletip
que divide el parámetrotipPercent
por un valor100
y multiplica el resultado por el parámetroamount
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
.
- En la siguiente línea del cuerpo de la función
calculateTip()
, llama a la funciónNumberFormat.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.
- En la llamada a función
NumberFormat.getCurrencyInstance()
, encadena el métodoformat()
y pásalo a la variabletip
como un parámetro.
NumberFormat.getCurrencyInstance().format(tip)
- Cuando Android Studio te lo solicite, importa esta clase.
import java.text.NumberFormat
- 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 clavereturn
delante de la sentenciaNumberFormat
:
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.
- En la función de componibilidad
EditNumberField()
, 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 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.
- 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() {
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:
- En la función
TipTimeScreen()
al final del bloqueColumn()
, agrega un elementoSpacer()
que admite composición y pasa una altura de24.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.
- Después del elemento
Spacer()
componible, agrega el siguiente elementoText
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.
- 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:
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:
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()
:
- Actualiza la definición de la función
EditNumberField()
a fin de elevar el estado agregando los parámetrosvalue
yonValueChange
.
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.
- 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ónTipTimeScreen()
:
@Composable
fun TipTimeScreen() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
//...
) {
//...
}
}
- Elevaste el estado a
TipTimeScreen()
y, ahora, lo pasas aEditNumberField()
. En la funciónTipTimeScreen()
, actualiza la llamada a la funciónEditNumberField
()
para usar el estado elevado:
EditNumberField(value = amountInput,
onValueChange = { amountInput = it }
)
- Usa la propiedad
tip
para mostrar el importe de la propina. Actualiza el parámetrotext
del elemento que admite composiciónText
para usar la variabletip
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.
- 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.
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.