Calcula una propina personalizada

1. Antes de comenzar

En este codelab, usarás el código de la solución del codelab Introducción al estado en Compose para compilar una calculadora interactiva de propinas que puede calcular y redondear automáticamente un importe de propina cuando ingresas el importe de la factura y el porcentaje de propina. Puedes ver la app final en esta imagen:

24370de6d667a700.png

Requisitos previos

  • Haber completado el codelab Estado de uso en Jetpack Compose
  • Poder agregar elementos Text y TextField que admiten composición a una app
  • Conocer la función remember, el estado, la elevación de estado y la diferencia entre las funciones que admiten composición con y sin estado

Qué aprenderás

  • Cómo agregar un botón de acción a un teclado virtual
  • Cómo configurar las acciones del teclado
  • Qué es un elemento componible Switch y cómo usarlo
  • Qué es el Inspector de diseño

Qué compilarás

  • Una app de Tip Time que calcula los importes de las propinas según el costo del servicio y el porcentaje de propina ingresados por el usuario

Requisitos

  • Android Studio
  • El código de solución del codelab Estado de uso en Jetpack Compose

2. Descripción general de la app de inicio

Este codelab comienza con la app de Tip Time del codelab anterior, que proporciona la interfaz de usuario necesaria para calcular una propina con un porcentaje fijo de propinas. El cuadro de texto Cost of Service (costo de servicio) le permite al usuario ingresar el costo del servicio. La app calcula y muestra el importe de la propina en un elemento Text que admite composición.

Obtén el código de inicio

Para comenzar, descarga el código de inicio:

Descargar ZIP

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

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

Puedes explorar el código en el repositorio de GitHub de Tip Calculator.

Ejecuta la app de Tip Time

  1. Abre el proyecto Tip Time en Android Studio y ejecuta la app en un emulador o dispositivo.
  2. Ingresa un costo de servicio. La app calculará y mostrará el importe de la propina automáticamente.

761df483de663721.png

En la implementación actual, el porcentaje de la propina se codifica al 15%. En este codelab, extenderás esta función con un campo de texto que le permite a la app calcular un porcentaje de propina personalizado y redondear el importe.

Agrega los recursos de strings necesarios

  1. En la pestaña Project, haz clic en res > values > strings.xml.
  2. Entre las etiquetas <resources> del archivo strings.xml, agrega estos recursos de strings:
<string name="how_was_the_service">Tip (%)</string>
<string name="round_up_tip">Round up tip?</string>

El archivo strings.xml debería verse como este fragmento de código, que incluye las strings del codelab anterior:

strings.xml

<resources>
   <string name="app_name">TipTime</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="cost_of_service">Cost of Service</string>
   <string name="how_was_the_service">Tip (%)</string>
   <string name="round_up_tip">Round up tip?</string>
   <string name="tip_amount">Tip Amount: %s</string>
</resources>
  1. Cambia la string Cost Of Service por una Bill Amount. En algunos países, servicio significa propina, por lo que este cambio evita confusiones.
  2. En la string Cost of Service, haz clic con el botón derecho en el name cost_of_service del atributo y selecciona Refactor > Rename. Se abrirá el diálogo Rename.

a2f301b95a8c0e3f.png

  1. En el cuadro de diálogo Rename, reemplaza cost_of _service por bill_amount y haz clic en Refactor. Esto actualiza todos los casos del recurso de strings cost_of_service en el proyecto, por lo que no necesitas cambiar el código de Compose de forma manual.

f525a371c2851d08.png

  1. En el archivo strings.xml, cambia el valor de string a Bill Amount desde Cost of Service:
<string name="bill_amount">Bill Amount</string>
  1. Navega al archivo MainActivity.kt y ejecuta la app. La etiqueta se actualizará en el cuadro de texto como se muestra en esta imagen:

el campo de texto muestra el importe de la factura en lugar del costo de servicio

3. Agrega un campo de texto de porcentaje de propina

Un cliente podría querer una propina mayor o menor según la calidad del servicio proporcionado y otros motivos. Para adecuarse a esto, la app debe permitir que el usuario calcule una propina personalizada. En esta sección, agregarás un campo de texto para que el usuario ingrese un porcentaje de propina personalizado, como se muestra en esta imagen:

47b5e8543e5eb754.png

Ya tienes un campo de texto para el Importe de facturación que admite composición en tu app, que es la función sin estado EditNumberField() componible. En el codelab anterior, elevaste el estado amountInput del elemento EditNumberField() que admite composición a la función TipTimeScreen(), lo que hizo que el elemento EditNumberField() no tuviera estado.

Para agregar un campo de texto, puedes volver a usar el mismo elemento que admite composición EditNumberField(), pero con una etiqueta diferente. Para realizar este cambio, debes pasar la etiqueta como parámetro, en lugar de codificarla dentro de la función EditNumberField() que admite composición.

Haz que la función que admite composición EditNumberField() sea reutilizable:

  1. En el archivo MainActivity.kt, en los parámetros de la función que admite composición EditNumberField(), agrega un recurso de strings label de tipo Int:
@Composable
fun EditNumberField(
   label: Int,
   value: String,
   onValueChange: (String) -> Unit
)
  1. Agrega un argumento modifier de tipo Modifier a la función que admite composición EditNumberField():
@Composable
fun EditNumberField(
   label: Int,
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
)
  1. En el cuerpo de la función, reemplaza el ID de recurso de strings codificado con el parámetro label:
@Composable
fun EditNumberField(
   //...
) {
   TextField(
       //...
       label = { Text(stringResource(label)) },
       //...
   )
}
  1. Para indicar que se espera que el parámetro label sea una referencia de recursos de strings, anota el parámetro de la función con la anotación @StringRes:
@Composable
fun EditNumberField(
   @StringRes label: Int,
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
)
  1. Importa lo siguiente:
import androidx.annotation.StringRes
  1. En el elemento TextField de la función EditNumberField() que admite composición, pasa el parámetro label a la función stringResource().
@Composable
fun EditNumberField(
   @StringRes label: Int,
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   TextField(
       //...
       label = { Text(stringResource(label)) },
       //...
   )
}
  1. En la llamada a la función EditNumberField() de la función TipTimeScreen(), establece el parámetro label en el recurso de strings R.string.bill_amount:
EditNumberField(
   label = R.string.bill_amount,
   value = amountInput,
   onValueChange = { amountInput = it }
)
  1. En el panel Design, haz clic en 2d40b921003ab5eb.png Build & Refresh. La IU de la app debería verse de la siguiente manera:

a84cd50c50235a9f.png

  1. En la función TipTimeScreen(), después de la llamada a función EditNumberField(), agrega otro campo de texto para el porcentaje de propina personalizada. Realiza una llamada a la función que admite composición EditNumberField() con estos parámetros:
EditNumberField(
   label = R.string.how_was_the_service,
   value = "",
   onValueChange = { }
)

Esto agrega otro cuadro de texto para el porcentaje de propina personalizada.

  1. En el panel Design, haz clic en 2d40b921003ab5eb.png Build & Refresh. La vista previa de la app ahora muestra un campo de texto Tip (%) (propina) como se puede ver en esta imagen:

9d2c01d577d077ae.png

  1. En la parte superior de la función TipTimeScreen(), agrega una propiedad var llamada tipInput para la variable de estado del campo de texto agregado. Usa mutableStateOf("") para inicializar la variable y rodea la llamada con la función remember:
var tipInput by remember { mutableStateOf("") }
  1. En la nueva llamada a función EditNumberField(), configura el parámetro con nombre value para la variable tipInput y, luego, actualiza la variable tipInput en la expresión lambda onValueChange:
EditNumberField(
   label = R.string.how_was_the_service,
   value = tipInput,
   onValueChange = { tipInput = it }
)
  1. En la función TipTimeScreen(), después de la definición de la variable tipInput, define una variable val llamada tipPercent que convierta la variable tipInput en un tipo Double, usa un operador Elvis y muestra 0.0 si el valor es null:
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
  1. En la función TipTimeScreen(), actualiza la llamada a la función calculateTip() y pasa la variable tipPercent como segundo parámetro:
val tip = calculateTip(amount, tipPercent)

El código para la función TipTimeScreen() debería verse de la siguiente manera:

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

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

   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(
           label = R.string.bill_amount,
           value = amountInput,
           onValueChange = { amountInput = it }
       )
       EditNumberField(
           label = R.string.how_was_the_service,
           value = tipInput,
           onValueChange = { tipInput = 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
       )
   }
}
  1. Ejecuta la app en un emulador o dispositivo y, luego, ingresa un importe para la factura y el porcentaje de propina. ¿La app calcula el importe correcto para la propina?

bdc482b015472300.png

4. Establece un botón de acción

En el codelab anterior, exploraste cómo usar la clase KeyboardOptions para establecer el tipo de teclado. En esta sección, aprenderás a configurar el botón de acción del teclado con el mismo KeyboardOptions. El botón de acción de teclado es el que se encuentra al final del teclado. Puedes ver algunos ejemplos en esta tabla:

Propiedad

Botón de acción del teclado

ImeAction.Search se usa cuando el usuario quiere ejecutar una búsqueda.

ImeAction.Send se usa cuando el usuario quiere enviar el texto en el campo de entrada.

ImeAction.Go se usa cuando el usuario quiere navegar al destino del texto en la entrada.

En esta tarea, establecerás dos botones de acción diferentes para los cuadros de texto:

  • Un botón de acción Next (siguiente) para el cuadro de texto Bill Amount (importe de la factura), que indica que el usuario ya completó la entrada actual y quiere pasar al siguiente cuadro de texto.
  • Un botón de acción Done (listo) para el cuadro de texto Tip % (porcentaje de propina), que indica que el usuario terminó de proporcionar la entrada.

Puedes ver ejemplos de teclados con estos botones de acción en las siguientes imágenes:

Agrega opciones del teclado:

  1. En la llamada a la función TextField() de la función EditNumberField(), pasa el constructor KeyboardOptions, un argumento con nombre imeAction establecido en un valor ImeAction.Next. Usa la función KeyboardOptions.Default.copy para usar las otras opciones predeterminadas, como el uso de mayúsculas y la corrección automática.
@Composable
fun EditNumberField(
   //...
) {
   TextField(
       //...
       keyboardOptions = KeyboardOptions.Default.copy(
           keyboardType = KeyboardType.Number,
           imeAction = ImeAction.Next
       )
   )
}
  1. Ejecuta la app en un emulador o dispositivo. El teclado ahora muestra el botón de acción Next, como se puede ver en esta imagen:

Sin embargo, te conviene usar dos botones de acción diferentes para los campos de texto. En breve, solucionarás este problema.

  1. Examina la función EditNumberField(). El parámetro keyboardOptions de la función TextField() está codificado. Si deseas crear botones de acción diferentes para los campos de texto, debes pasar el objeto KeyboardOptions como argumento, y lo harás en el paso siguiente.
// No need to copy, just examine the code.
fun EditNumberField(
   @StringRes label: Int,
   value: String,
   onValueChange: (String) -> Unit
) {
   TextField(
       //...
       keyboardOptions = KeyboardOptions.Default.copy(
          keyboardType = KeyboardType.Number,
          imeAction = ImeAction.Next
       )
   )
}
  1. En la definición de la función EditNumberField(), agrega un parámetro keyboardOptions de tipo KeyboardOptions. En el cuerpo de la función, asigna el parámetro con nombre keyboardOptions de la función TextField():
@Composable
fun EditNumberField(
   @StringRes label: Int,
   keyboardOptions: KeyboardOptions,
   value: String,
   onValueChange: (String) -> Unit
){
   TextField(
       //...
       keyboardOptions = keyboardOptions
   )
}
  1. En la función TipTimeScreen(), actualiza la primera llamada a la función EditNumberField() y pasa el parámetro con nombre keyboardOptions para el campo de texto Bill amount.
EditNumberField(
   label = R.string.bill_amount,
   keyboardOptions = KeyboardOptions(
       keyboardType = KeyboardType.Number,
       imeAction = ImeAction.Next
   ),
   value = amountInput,
   onValueChange = { amountInput = it }
)
  1. En la segunda llamada a la función EditNumberField(), cambia el imeAction del campo de texto Tip % a ImeAction.Done. Tu función debería verse como este fragmento de código:
EditNumberField(
   label = R.string.how_was_the_service,
   keyboardOptions = KeyboardOptions(
       keyboardType = KeyboardType.Number,
       imeAction = ImeAction.Done
   ),
   value = tipInput,
   onValueChange = { tipInput = it }
)
  1. Ejecuta la app. Se mostrarán los botones de acción Next y Done, como se puede ver en estas imágenes:

  1. Ingresa un importe de factura y haz clic en el botón de acción Next. Luego, ingresa un porcentaje de propina y haz clic en el botón de acción Done. No ocurre nada porque aún no agregaste ninguna funcionalidad a los botones. Puedes hacerlo en la próxima sección.

5. Establece acciones del teclado

En esta sección, implementarás la funcionalidad para mover el enfoque al siguiente campo de texto y cerrar el teclado a fin de mejorar la experiencia del usuario con la clase KeyboardActions, que permite a los desarrolladores especificar acciones que se activan en respuesta a la acción de IME (editor de método de entrada) en el teclado en pantalla. Un ejemplo de una acción de IME es cuando el usuario hace clic en el botón de acción Next o Done.

Implementarás lo siguiente:

  • En la acción Next, mueve el enfoque al siguiente campo de texto (el cuadro de texto Tip %).
  • En la acción Done, cierra el teclado virtual.
  1. En la función EditNumberField(), agrega una variable val llamada focusManager y asígnale un valor de la propiedad LocalFocusManager.current:
val focusManager = LocalFocusManager.current

La interfaz LocalFocusManager se usa para controlar el enfoque en Compose. Usarás esta variable para mover el enfoque a los cuadros de texto y fuera de ellos.

  1. Importa el elemento import androidx.compose.ui.platform.LocalFocusManager.
  2. En la firma de la función EditNumberField(), agrega otro parámetro keyboardActions de tipo KeyboardActions:
@Composable
fun EditNumberField(
   @StringRes label: Int,
   keyboardOptions: KeyboardOptions,
   keyboardActions: KeyboardActions,
   value: String,
   onValueChange: (String) -> Unit
) {
   //...
}
  1. En el cuerpo de la función EditNumberField(), actualiza la llamada a la función TextField() y establece el parámetro keyboardActions en el parámetro que se pasó keyboardActions.
@Composable
fun EditNumberField(
   //...
) {
   TextField(
       //...
       keyboardActions = keyboardActions
   )
}

Ahora puedes personalizar los campos de texto con diferentes funciones para cada botón de acción.

  1. En la llamada a función TipTimeScreen(), actualiza la primera llamada a función EditNumberField() para que incluya un parámetro con nombre keyboardActions como argumento nuevo. Asígnale un valor, KeyboardActions( onNext = { } ):
// Bill amount text field
EditNumberField(
   //...
   keyboardActions = KeyboardActions(
       onNext = { }
   ),
   //...
)

La expresión lambda del parámetro onNext nombrado se ejecuta cuando el usuario presiona el botón de acción Next en el teclado.

  1. Define la lambda, solicita FocusManager para mover el enfoque hacia abajo al siguiente elemento que admite composición, Tip %. En la expresión lambda, llama a la función moveFocus() en el objeto focusManager y pasa el argumento FocusDirection.Down:
// Bill amount text field
EditNumberField(
   label = R.string.bill_amount,
   keyboardOptions = KeyboardOptions(
       keyboardType = KeyboardType.Number,
       imeAction = ImeAction.Next
   ),
   keyboardActions = KeyboardActions(
       onNext = { focusManager.moveFocus(FocusDirection.Down) }
   ),
   value = amountInput,
   onValueChange = { amountInput = it }
)

La función moveFocus() mueve el enfoque en la dirección especificada, que se encuentra hacia abajo hasta el campo de texto (en este caso, Tip %).

  1. Importa lo siguiente:
import androidx.compose.ui.focus.FocusDirection
  1. Agrega una implementación similar al campo de texto Tip %. La diferencia es que debes definir un parámetro llamado onDone, en lugar de onNext.
// Tip% text field
EditNumberField(
   //...
   keyboardActions = KeyboardActions(
       onDone = { }
   ),
   //...
)
  1. Una vez que el usuario ingresa la propina personalizada, la acción Done en el teclado debe borrar el enfoque, lo que, a su vez, cierra el teclado. Define la lambda y solicita FocusManager para borrar el enfoque. En la expresión lambda, llama a la función clearFocus() en el objeto focusManager:
EditNumberField(
   label = R.string.how_was_the_service,
   keyboardOptions = KeyboardOptions(
       keyboardType = KeyboardType.Number,
       imeAction = ImeAction.Done
   ),
   keyboardActions = KeyboardActions(
       onDone = { focusManager.clearFocus() }),
   value = tipInput,
   onValueChange = { tipInput = it }
)

La función clearFocus() borra el enfoque del componente enfocado.

  1. Ejecuta la app. Las acciones del teclado ahora cambian el enfoque del componente, como se puede ver en este GIF:

3164e7a2f39a2d7b.gif

6. Agrega un interruptor

Un interruptor activa o desactiva el estado de un solo elemento. Existen dos estados en un interruptor que permiten al usuario seleccionar entre dos opciones. Un interruptor consiste en un círculo y una barra, como se puede ver en estas imágenes:

1. Círculo
2. Barra

El interruptor es un control de selección que se puede usar para ingresar decisiones o declarar preferencias, como los parámetros que se pueden ver en estas imágenes:

a90c4e22e48b30e0.png

El usuario puede arrastrar el círculo hacia adelante y hacia atrás para elegir la opción seleccionada, o simplemente presionar el botón de interruptor. Puedes ver otro ejemplo de un interruptor en este GIF, en el que la configuración de Opciones visuales está activada (Modo oscuro):

91b7bd7a6e02e5ff.gif

Para obtener más información, consulta la documentación sobre interruptores.

Usas el elemento componible Switch para que el usuario pueda elegir si redondea la propina al número entero más cercano como se muestra en la siguiente imagen:

cf89a61484296bab.png

Agrega una fila para los elementos que admiten composición Text y Switch:

  1. Después de la función EditNumberField(), agrega una función que admite composición RoundTheTipRow() y pasa un Modifier predeterminado, por ejemplo, argumentos similares a la función EditNumberField():
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
  1. Implementa la función RoundTheTipRow(), agrega un elemento de diseño que admite composición Row con el siguiente modifier para establecer el ancho de los elementos secundarios al máximo en la pantalla, centrar la alineación y garantizar un tamaño de 48 dp:
Row(
   modifier = Modifier
       .fillMaxWidth()
       .size(48.dp),
   verticalAlignment = Alignment.CenterVertically
) {
}
  1. Importa lo siguiente:
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Size
  1. En el bloque de lambda del elemento de diseño que admite composición Row, agrega un elemento Text que admita composición y usa el recurso de strings R.string.round_up_tip para mostrar una string Round up tip?:
Text(text = stringResource(R.string.round_up_tip))
  1. Después del elemento Text que admite composición, agrega un elemento Switch que también admite composición, pasa un parámetro con nombre checked establecido en roundUp y un parámetro con nombre onCheckedChange establecido en onRoundUpChanged.
Switch(
    checked = roundUp,
    onCheckedChange = onRoundUpChanged,
)

Esta tabla contiene información sobre estos parámetros, que son los mismos que definiste para la función RoundTheTipRow():

Parámetro

Descripción

checked

Indica si el interruptor está marcado. Este es el estado del elemento que admite composición Switch.

onCheckedChange

Es la devolución de llamada a la que se llamará cuando se haga clic en el interruptor.

  1. Importa lo siguiente:
import androidx.compose.material.Switch
  1. En la función RoundTipRow(), agrega un parámetro roundUp de tipo Boolean y una función lambda onRoundUpChanged que tome un Boolean, pero no muestre nada:
@Composable
fun RoundTheTipRow(
   roundUp: Boolean,
   onRoundUpChanged: (Boolean) -> Unit,
   modifier: Modifier = Modifier
)

Con esta acción, se eleva el estado del interruptor.

  1. En el elemento Switch que admite composición, agrega este modifier para alinear el elemento Switch que admite composición al final de la pantalla:
       Switch(
           modifier = modifier
               .fillMaxWidth()
               .wrapContentWidth(Alignment.End),
           //...
       )
  1. Importa lo siguiente:
import androidx.compose.foundation.layout.wrapContentWidth
  1. En la función TipTimeScreen(), agrega una variable var para el estado del elemento Switch que admite composición. Crea una variable var llamada roundUp establecida en mutableStateOf(), con false como argumento predeterminado. Rodea la llamada con remember { }.
fun TipTimeScreen() {
   //...
   var roundUp by remember { mutableStateOf(false) }

   //...
   Column(
       ...
   ) {
     //...
  }
}

Esta es la variable del estado que admite composición Switch y el valor falso será el predeterminado.

  1. En el bloque Column de la función TipTimeScreen(), después del campo de texto Tip %, llama a la función RoundTheTipRow() con los siguientes argumentos: un parámetro con nombre roundUp establecido en roundUp y un parámetro con nombre onRoundUpChanged establecido en una devolución de llamada lambda que actualice el valor roundUp:
@Composable
fun TipTimeScreen() {
   //...

   Column(
       ...
   ) {
       Text(
           ...
       )
       Spacer(...)
       EditNumberField(
           ...
       )
       EditNumberField(
           ...
       )
       RoundTheTipRow(roundUp = roundUp, onRoundUpChanged = { roundUp = it })
       Spacer(...)
       Text(
           ...
       )
   }
}

Se mostrará la fila Round up tip (Redondear propina).

  1. Ejecuta la app. Esta muestra el interruptor Round up tip? (¿Redondear propina?), pero el círculo es apenas visible, como se ve en esta imagen:

Interruptores no seleccionados y seleccionados con números que identifican sus 2 elementos y estados1. Círculo
2. Barra

En los próximos pasos, mejorarás la visibilidad del círculo volviéndolo gris oscuro.

  1. En el elemento que admite composición Switch() de la función RoundTheTipRow(), agrega un parámetro llamado colors.
  2. Configura ese parámetro colors en una función SwitchDefaults.colors() que acepte un parámetro llamado uncheckedThumbColor establecido en un argumento Color.DarkGray.
Switch(
   //...
   colors = SwitchDefaults.colors(
       uncheckedThumbColor = Color.DarkGray
   )
)
  1. Importa lo siguiente:
import androidx.compose.material.SwitchDefaults
import androidx.compose.ui.graphics.Color

La función que admite composición RoundTheTipRow() ahora debería verse como este fragmento de código:

@Composable
fun RoundTheTipRow(roundUp: Boolean, onRoundUpChanged: (Boolean) -> Unit) {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .size(48.dp),
       verticalAlignment = Alignment.CenterVertically
   ) {
       Text(stringResource(R.string.round_up_tip))
       Switch(
           modifier = Modifier
               .fillMaxWidth()
               .wrapContentWidth(Alignment.End),
           checked = roundUp,
           onCheckedChange = onRoundUpChanged,
           colors = SwitchDefaults.colors(
               uncheckedThumbColor = Color.DarkGray
           )
       )
   }
}
  1. Ejecuta la app. Como puedes ver en la imagen, el círculo del interruptor es de otro color.

24370de6d667a700.png

  1. Ingresa un importe de la factura y un porcentaje de propina y selecciona el interruptor Round up tip?. El importe de la propina no se redondea porque todavía necesitas actualizar la función calculateTip(); lo haremos en la siguiente sección.

Actualiza la función calculateTip() para redondear la propina.

Modifica la función calculateTip() de modo que acepte una variable Boolean para redondear la propina al número entero más cercano:

  1. Para redondear la propina, la función calculateTip() debe conocer el estado del interruptor, que es un Boolean. En la función calculateTip(), agrega un parámetro roundUp de tipo Boolean:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0,
   roundUp: Boolean
): String {
   //...
}
  1. En la función calculateTip(), antes de la sentencia return, agrega una condición if() que verifique el valor roundUp. Si roundUp es true, define una variable tip, establécela en kotlin.math.ceil() y pasa la función tip como argumento:
if (roundUp)
   tip = kotlin.math.ceil(tip)

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

private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
   var tip = tipPercent / 100 * amount
   if (roundUp)
       tip = kotlin.math.ceil(tip)
   return NumberFormat.getCurrencyInstance().format(tip)
}
  1. En la función TipTimeScreen(), actualiza la llamada a la función calculateTip() y pasa un parámetro roundUp:
val tip = calculateTip(amount, tipPercent, roundUp)
  1. Ejecuta la app. Ahora, esta redondea el importe de la propina, como puedes ver en las siguientes imágenes:

7. Obtén 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

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.

8. Conclusión

¡Felicitaciones! Agregaste la funcionalidad de propina personalizada a tu app de Tip Time. Ahora, la app permite que los usuarios ingresen un porcentaje de propina personalizado y redondeen el importe correspondiente. Comparte tu trabajo en redes sociales con el hashtag #AndroidBasics.

Más información