Cómo calcular la propina

1. Antes de comenzar

En este codelab, escribirás código de modo que la calculadora de propinas se adapte a la IU que creaste en el codelab anterior: Cómo crear diseños XML para Android.

Requisitos previos

Qué aprenderás

  • La estructura básica de las apps para Android
  • Cómo leer valores de la IU en tu código y manipularlos
  • Cómo usar la vinculación de vista en lugar de findViewById() para escribir con mayor facilidad código que interactúe con vistas
  • Cómo trabajar con números decimales en Kotlin con el tipo de datos Double
  • Cómo darles formato de moneda a los números
  • Cómo usar parámetros de cadena para crear cadenas de forma dinámica
  • Cómo usar Logcat en Android Studio para encontrar problemas en tu app

Qué compilarás

  • Una app para calcular propinas con un botón Calculate (Calcular) que funcione

Requisitos

  • Una computadora con la versión estable más reciente de Android Studio instalada
  • Código de partida para la app Tip Time que contiene el diseño para una calculadora de propinas

2. Descripción general de la app de partida

La app Tip Time del último codelab tiene toda la IU necesaria para una calculadora de propinas, pero no el código para realizar la acción. Incluye un botón Calculate (Calcular), pero todavía no funciona. Cost of Service EditText le permite al usuario introducir el costo del servicio. Una lista de RadioButtons le permite al usuario seleccionar el porcentaje de propina, y un elemento Switch le permite al usuario elegir si la propina se debe redondear hacia arriba o no. El importe de la propina se muestra en un elemento TextView y, por último, un Button Calculate le indica a la app que obtenga los datos de los otros campos y calcule el importe de la propina. En este codelab, se trata este tema.

ebf5c40d4e12d4c7.png

Estructura del proyecto de app

En tu IDE, un proyecto de app consta de varias partes, que incluyen el código Kotlin, los diseños XML y otros recursos como imágenes y cadenas. Antes de hacer cambios en la app, te recomendamos que aprendas cómo funciona.

  1. Abre el proyecto Tip Time en Android Studio.
  2. Si no se muestra la ventana Project, selecciona la pestaña Project en el lado izquierdo de Android Studio.
  3. Si todavía no se seleccionó, elige la vista Android en el menú desplegable.

2a83e2b0aee106dd.png

  • La carpeta java para archivos Kotlin (o archivos Java)
  • MainActivity: Clase en la que irá todo el código Kotlin para la lógica de la calculadora de propinas
  • Carpeta res para recursos de apps
  • activity_main.xml: Archivo de diseño de tu app para Android
  • strings.xml: Contiene recursos de cadenas de tu app para Android
  • Carpeta Gradle Scripts

Gradle es el sistema de compilación automatizado que usa Android Studio. Cada vez que cambias el código, agregas un recurso o realizas otros cambios en tu app, Gradle determina qué se cambió y toma las medidas necesarias para volver a compilar tu app. Además, instala tu app en el emulador o en un dispositivo físico, y controla su ejecución.

Existen otras carpetas y otros archivos que forman parte de la compilación de tu app, pero estos son los principales con los que trabajarás en este codelab y en los siguientes.

3. Vinculación de vista

Para calcular la propina, tu código deberá acceder a todos los elementos de la IU para leer la entrada del usuario. Es posible que recuerdes de codelabs anteriores que tu código debe encontrar una referencia a un elemento View, por ejemplo, Button o TextView, antes de que pueda llamar a los métodos en el elemento View o acceder a sus atributos. El framework de Android brinda un método, findViewById(), que hace exactamente lo que necesitas; dado el ID de un elemento View, devuelve una referencia a este. Este enfoque funciona, pero, a medida que agregas más vistas a tu app y la IU se vuelve más compleja, usar findViewById() puede complicarse.

Para motivos de conveniencia, Android también brinda una función que se denomina vinculación de vista. Con un poco más de trabajo al principio, la vinculación de vista permite que sea mucho más fácil y rápido llamar a métodos en las vistas de tu IU. Deberás habilitar la vinculación de vista para tu app en Gradle y realizar algunos cambios en el código.

Cómo habilitar la vinculación de vista

  1. Abre el archivo build.gradle de la app (Gradle Scripts > build.gradle (Module: Tip_Time.app))
  2. En la sección android, agrega las siguientes líneas:
buildFeatures {
    viewBinding = true
}
  1. Ten en cuenta el mensaje Gradle files have changed since last project sync.
  2. Presiona Sync Now.

349d99c67c2f40f1.png

Después de unos momentos, deberías ver un mensaje en la parte inferior de la ventana de Android Studio: Gradle sync finished. Puedes cerrar el archivo build.gradle si lo deseas.

Cómo inicializar el objeto de vinculación

En codelabs anteriores, se trató el método onCreate() en la clase MainActivity. Es uno de los primeros elementos que se llaman cuando se inicia tu app y se inicializa la clase MainActivity. En lugar de llamar a findViewById() para cada View en tu app, deberás crear e inicializar un objeto de vinculación una vez.

674d243aa6f85b8b.png

  1. Abre MainActivity.kt (app > java > com.example.tiptime > MainActivity).
  2. Reemplaza todo el código existente para la clase MainActivity con este código para configurar la clase MainActivity de modo que puedas usar la vinculación de vista:
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}
  1. Esta línea declara una variable de nivel superior para la clase del objeto de vinculación. Se define en este nivel porque se usará en varios métodos de la clase MainActivity.
lateinit var binding: ActivityMainBinding

La palabra clave lateinit es algo nuevo. Es una promesa de que tu código inicializará la variable antes de usarla. De lo contrario, tu app fallará.

  1. Esta línea inicializa el objeto binding que usarás para acceder a Views en el diseño activity_main.xml.
binding = ActivityMainBinding.inflate(layoutInflater)
  1. Configura la vista de contenido de la actividad. En lugar de pasar el ID de recurso del diseño, R.layout.activity_main, especifica la raíz de la jerarquía de vistas en tu app, binding.root.
setContentView(binding.root)

Es posible que recuerdes la idea de las vistas superiores y las vistas secundarias; la raíz se conecta con todas estas.

Ahora, cuando necesitas una referencia a un elemento View en tu app, puedes obtenerla del objeto binding en lugar de llamar a findViewById(). El objeto binding define automáticamente referencias para cada View de tu app que tiene un ID. El uso de la vinculación de vista es tan conciso que, con frecuencia, no necesitarás crear una variable para tener la referencia de un elemento View; solo debes usarla directamente desde el objeto de vinculación.

// Old way with findViewById()
val myButton: Button = findViewById(R.id.my_button)
myButton.text = "A button"

// Better way with view binding
val myButton: Button = binding.myButton
myButton.text = "A button"

// Best way with view binding and no extra variable
binding.myButton.text = "A button"

Es genial, ¿no?

4. Cómo calcular la propina

El proceso para calcular la propina comienza cuando el usuario presiona el botón Calculate. Este proceso incluye verificar la IU para ver el costo del servicio y el porcentaje de propina que el usuario desea dejar. Con esta información, calcularás el importe total del cargo del servicio, y se mostrará el importe de la propina.

Cómo agregarle un objeto de escucha de clics al botón

El primer paso implica agregar un objeto de escucha de clics para especificar qué debe hacer el botón Calculate cuando el usuario lo presiona.

  1. En MainActivity.kt, en onCreate(), después de la llamada a setContentView(), configura un objeto de escucha de clics en el botón Calculate y haz que llame a calculateTip().
binding.calculateButton.setOnClickListener{ calculateTip() }
  1. Todavía en la clase MainActivity, pero fuera del objeto onCreate(), agrega un método asistente llamado calculateTip().
fun calculateTip() {

}

Aquí agregarás el código para verificar la IU y calcular la propina.

MainActivity.kt

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.calculateButton.setOnClickListener{ calculateTip() }
    }

    fun calculateTip() {

    }
}

Cómo obtener el costo del servicio

Para calcular la propina, lo primero que necesitas es el costo del servicio. El texto se almacena en EditText, pero lo necesitarás como un número para que puedas usarlo en los cálculos. Es posible que recuerdes el tipo Int de otros codelabs, pero un elemento Int solo puede contener números enteros. Para usar un número decimal en tu app, usa el tipo de datos llamado Double en lugar de Int. Puedes leer más sobre los tipos de datos numéricos en Kotlin, en la documentación. Kotlin brinda un método para convertir un objeto String en Double, que se llama toDouble().

  1. Primero, obtén el texto para el costo del servicio. En el método calculateTip(), obtén el atributo de texto de Cost of Service EditText y asígnalo a una variable llamada stringInTextField. Recuerda que puedes acceder al elemento de la IU con el objeto binding y que puedes referirte al elemento de la IU, según su nombre de ID de recurso en mayúsculas y minúsculas.
val stringInTextField = binding.costOfService.text

Observa el elemento .text al final. La primera parte, binding.costOfService, hace referencia al elemento de la IU para el costo del servicio. Agregar .text al final indica tomar ese resultado (un objeto EditText) y obtener la propiedad text de este, lo que se conoce como encadenamiento y es un patrón muy común en Kotlin.

  1. Luego, convierte el texto en un número decimal. Llama al objeto toDouble() en stringInTextField y almacénalo en una variable llamada cost.
val cost = stringInTextField.toDouble()

Sin embargo, eso no funciona. Se debe llamar a toDouble() en un elemento String. Resulta que el atributo text de un objeto EditText es Editable, porque representa un texto que se puede cambiar. Afortunadamente, puedes convertir un objeto Editable en String si llamas a toString().

  1. Llama al objeto toString() en binding.costOfService.text para convertirlo en un elemento String:
val stringInTextField = binding.costOfService.text.toString()

Ahora, stringInTextField.toDouble() funcionará.

En este punto, el método calculateTip() debería verse de la siguiente manera:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
}

Cómo obtener el porcentaje de propina

Hasta ahora, tienes el costo del servicio. Ahora, necesitas el porcentaje de propina, que el usuario seleccionó de un objeto RadioGroup de RadioButtons.

  1. En calculateTip(), obtén el atributo checkedRadioButtonId de tipOptions RadioGroup y asígnalo a una variable llamada selectedId.
val selectedId = binding.tipOptions.checkedRadioButtonId

Ahora, sabes que se seleccionó RadioButton, uno de R.id.option_twenty_percent, R.id.option_eighteen_percent o R.id.fifteen_percent, pero necesitas el porcentaje correspondiente. Puedes escribir una serie de sentencias if/else, pero es mucho más fácil usar una expresión when.

  1. Agrega las siguientes líneas para obtener el porcentaje de propina.
val tipPercentage = when (selectedId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

En este punto, el método calculateTip() debería verse de la siguiente manera:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
}

Cómo calcular la propina y redondearla

Ahora que tienes el costo del servicio y el porcentaje de propina, calcularla es sencillo: la propina es el costo por el porcentaje de la propina, propina = costo del servicio * porcentaje de propina. De manera opcional, ese valor se puede redondear hacia arriba.

  1. En calculateTip() después del otro código que agregaste, multiplica tipPercentage por cost y asigna el resultado a una variable llamada tip.
var tip = tipPercentage * cost

Ten en cuenta que se usa var en lugar de val, ya que podrías tener que redondear el valor si el usuario seleccionó esa opción, por lo que este valor puede cambiar.

En el caso de los elementos Switch, puedes verificar el atributo isChecked para ver si el interruptor está "activado".

  1. Asigna el atributo isChecked del interruptor para redondear a una variable llamada roundUp.
val roundUp = binding.roundUpSwitch.isChecked

El término "redondear" implica ajustar un número decimal hacia arriba o hacia abajo al valor de número entero más cercano; pero, en este caso, solo quieres redondear hacia arriba o alcanzar el límite. Puedes usar la función ceil() para hacerlo. Existen varias funciones con ese nombre, pero la que necesitas se define en kotlin.math. Puedes agregar una sentencia import, pero, en este caso, es más fácil solo informar a Android Studio cuál es tu intención con el uso de kotlin.math.ceil().

32c29f73a3f20f93.png

Si deseas usar varias funciones matemáticas, sería más fácil agregar una sentencia import.

  1. Agrega una sentencia if que le asigne el límite de la propina a la variable tip si el elemento roundUp es verdadero.
if (roundUp) {
    tip = kotlin.math.ceil(tip)
}

En este punto, el método calculateTip() debería verse de la siguiente manera:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
    var tip = tipPercentage * cost
    val roundUp = binding.roundUpSwitch.isChecked
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
}

Cómo darle formato a la propina

Tu app ya casi funciona. Calculaste la propina; ahora, solo tienes que darle formato y mostrarla.

Como se puede esperar, Kotlin brinda métodos para darles formato a diferentes tipos de números. Pero el importe de la propina es un poco diferente: representa un valor de moneda. Diferentes países utilizan monedas distintas y tienen reglas diferentes para darles formato a los números decimales. Por ejemplo, en dólares estadounidenses, 1234.56 tendría el formato $1,234.56, pero en euros, sería €1.234,56. Afortunadamente, el framework de Android brinda métodos para darles formato de moneda a los números, de modo que no necesitas conocer todas las posibilidades. El sistema da el formato de moneda automáticamente según el idioma y otras configuraciones que el usuario haya elegido en su teléfono. Obtén más información sobre NumberFormat en la documentación para desarrolladores de Android.

  1. En calculateTip() después de tu otro código, llama a NumberFormat.getCurrencyInstance().
NumberFormat.getCurrencyInstance()

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

  1. Con el formateador de números, encadena una llamada al método format() con tip y asigna el resultado a una variable llamada formattedTip.
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
  1. Ten en cuenta que NumberFormat se dibuja en rojo, ya que Android Studio no puede detectar automáticamente a qué versión de NumberFormat haces referencia.
  2. Coloca el cursor sobre NumberFormat y elige Import en la ventana emergente que aparece. d9d2f92d5ef01df6.png
  3. En la lista de importaciones posibles, elige NumberFormat (java.text). Android Studio agrega una sentencia import en la parte superior del archivo MainActivity, y NumberFormat ya no está en rojo.

Cómo mostrar la propina

Ahora, es momento de mostrar la propina en el elemento TextView del importe de la propina de la app. Puedes asignar formattedTip al atributo text, pero sería bueno etiquetar qué representa el importe. En EE. UU. con inglés, es posible que se muestre Tip Amount: USD 12.34 (Importe de la propina: USD 12.34), pero en otros idiomas es posible que el número deba aparecer al principio o incluso en el medio de la cadena. El framework de Android brinda un mecanismo para esto, que se denomina parámetros de cadena, para que la persona que traduzca tu app pueda cambiar el lugar en que aparece el número si es necesario.

  1. Abre strings.xml (app > res > values > strings.xml)
  2. Cambia la cadena tip_amount de Tip Amount a Tip Amount: %s.
<string name="tip_amount">Tip Amount: %s</string>

El elemento %s es donde se insertará la moneda con formato.

  1. Ahora, configura el texto de tipResult. Regresa al método calculateTip() en MainActivity.kt, llama al objeto getString(R.string.tip_amount, formattedTip) y asígnalo al atributo text de TextView del resultado de la propina.
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)

En este punto, el método calculateTip() debería verse de la siguiente manera:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
    var tip = tipPercentage * cost
    val roundUp = binding.roundUpSwitch.isChecked
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
    binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}

Ya casi terminas. Cuando desarrollas tu app (y obtienes la vista previa), es útil tener un marcador de posición para ese elemento TextView.

  1. Abre activity_main.xml (app > res > layout > activity_main.xml).
  2. Busca el elemento tip_result TextView.
  3. Quita la línea con el atributo android:text.
android:text="@string/tip_amount"
  1. Agrega una línea para el atributo tools:text que se configuró en Tip Amount: $10.
tools:text="Tip Amount: $10"

Como es solo un marcador de posición, no es necesario que extraigas la cadena en un recurso. No aparecerá cuando ejecutes tu app.

  1. Ten en cuenta que el texto de las herramientas aparece en el editor de diseño.
  2. Ejecuta tu app. Introduce un importe para el costo y selecciona algunas opciones. Luego, presiona el botón Calculate.

42fd6cd5e24ca433.png

¡Felicitaciones! ¡Funciona! Si no obtienes el importe correcto de la propina, regresa al paso 1 de esta sección y asegúrate de haber realizado todos los cambios necesarios en el código.

5. Cómo probar y depurar

Ejecutaste la app en varios pasos para asegurarte de que haga lo que desees, pero ahora es el momento de realizar más pruebas.

Por ahora, piensa en cómo se mueve la información por la app en el método calculateTip() y qué podría salir mal en cada paso.

Por ejemplo, ¿qué sucedería en esta línea:

val cost = stringInTextField.toDouble()

si stringInTextField no representara ningún número? ¿Qué sucedería si el usuario no introdujera texto y el elemento stringInTextField estuviera vacío?

  1. Ejecuta tu app en el emulador, pero, en lugar de usar Run > Run 'app', usa Run > Debug 'app'.
  2. Prueba diferentes combinaciones de costos, importes de propinas y redondeos de propinas o no, y verifica que se muestren los resultados previstos para cada caso cuando presiones Calculate.
  3. Ahora, intenta borrar todo el texto del campo Cost of Service y presiona Calculate. ¡Vaya! El programa falla.

Cómo depurar la falla

El primer paso para abordar un error implica averiguar qué sucedió. Android Studio guarda un registro de lo que sucede en el sistema; puedes usarlo para detectar los errores.

  1. Presiona el botón Logcat en la parte inferior de Android Studio o selecciona View > Tool Windows > Logcat en los menús.

1b68ee5190018c8a.png

  1. La ventana Logcat aparece en la parte inferior de Android Studio, con texto de apariencia extraña.22139575476ae9d.png

El texto es un seguimiento de pila, una lista de los métodos a los que se llamó cuando se produjo la falla.

  1. Desplázate hacia arriba en el texto Logcat hasta encontrar una línea que incluya el texto FATAL EXCEPTION.
2020-06-24 10:09:41.564 24423-24423/com.example.tiptime E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.tiptime, PID: 24423
    java.lang.NumberFormatException: empty String
        at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
        at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.lang.Double.parseDouble(Double.java:538)
        at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
        at com.example.tiptime.MainActivity$onCreate$1.onClick(MainActivity.kt:17)
  1. Lee hacia abajo hasta encontrar la línea con NumberFormatException.
java.lang.NumberFormatException: empty String

A la derecha, dice empty String. El tipo de excepción te indica que se trataba de un error relacionado con un formato de número, y el resto te indica la raíz del problema: se encontró un elemento String vacío cuando debería haber sido un elemento String con un valor.

  1. Continúa leyendo hacia abajo y verás algunas llamadas a parseDouble().
  2. Debajo de esas llamadas, busca la línea con calculateTip. Observa que también se incluye tu clase MainActivity.
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
  1. Presta atención a esa línea y podrás ver exactamente en qué parte del código se realizó la llamada: línea 22 en MainActivity.kt (si introdujiste el código de manera diferente, puede ser un número distinto). Esa línea convierte el elemento String en un objeto Double y asigna el resultado a la variable cost.
val cost = stringInTextField.toDouble()
  1. Consulta la documentación de Kotlin para obtener el método toDouble() que funciona en un elemento String. El método se conoce como String.toDouble().
  2. La página dice "Exceptions: NumberFormatException - if the string is not a valid representation of a number".

Una excepción es la manera en que el sistema indica que existe un problema. En este caso, el problema es que toDouble() no pudo convertir el elemento String vacío en Double. Aunque EditText tiene inputType=numberDecimal, es posible ingresar algunos valores que toDouble() no pueda procesar, como una cadena vacía.

Cómo obtener información sobre el valor nulo

No funciona llamar a toDouble() en una cadena vacía o una cadena que no representa un número decimal válido. Afortunadamente, Kotlin también brinda un método llamado toDoubleOrNull() y aborda estos problemas. Devuelve un número decimal si es posible o devuelve null si existe un problema.

Nulo es un valor especial que significa "sin valor". Es diferente a un elemento Double que tiene un valor de 0.0 o un objeto String vacío con cero caracteres, "". Null significa sin valor, sin Double o sin String. Muchos métodos esperan un valor, y es posible que no sepan cómo controlar null y que se detengan, lo que implica que la app falla, así que Kotlin intenta limitar el uso de null. Obtendrás más información sobre este tema en las próximas lecciones.

Tu app puede verificar que se devuelva null desde toDoubleOrNull() y realizar acciones de manera diferente en ese caso, de modo que la app no falle.

  1. En calculateTip(), cambia la línea que declara la variable cost para llamar a toDoubleOrNull() en lugar de llamar a toDouble().
val cost = stringInTextField.toDoubleOrNull()
  1. Luego de esa línea, agrega una sentencia para verificar si cost es null y, de ser así, devolver desde el método. La instrucción return implica salir del método sin ejecutar el resto de las instrucciones. Si el método necesitara devolver un valor, lo especificarías con una instrucción return con una expresión.
if (cost == null) {
    return
}
  1. Vuelve a ejecutar tu app.
  2. Sin texto en el campo Cost of Service, presiona Calculate. Esta vez, la app no falla. ¡Bien hecho! Encontraste y corregiste el error.

Cómo controlar otro caso

No todos los errores producen fallas en la app, ya que, con frecuencia, los resultados pueden ser potencialmente confusos para el usuario.

Este es otro caso para considerar. ¿Qué sucederá si el usuario realiza lo siguiente?:

  1. introduce un importe válido para el costo
  2. presiona Calculate para calcular la propina
  3. elimina el costo
  4. vuelve a presionar Calculate

La primera vez, se calculará la propina y se mostrará según lo previsto. La segunda vez, se devolverá antes el método calculateTip() debido a la verificación que acabas de agregar, pero la app continuará mostrando el importe previo de la propina. Eso puede ser confuso para el usuario, así que agrega código a fin de borrar el importe de la propina si existe algún problema.

  1. Para confirmar este problema, introduce un costo válido y presiona Calculate. Luego, borra el texto y vuelve a presionar Calculate. Se debe continuar mostrando el primer valor de la propina.
  2. Dentro del elemento if recién agregado, antes de la sentencia return, agrega una línea para configurar el atributo text de tipResult en una cadena vacía.
if (cost == null) {
    binding.tipResult.text = ""
    return
}

De esta manera, se borrará el importe de la propina antes de que se devuelva desde calculateTip().

  1. Vuelve a ejecutar la app y prueba el caso anterior. El primer valor de la propina debería desaparecer cuando presiones Calculate la segunda vez.

¡Felicitaciones! Creaste una app para calcular propinas para Android y controlaste algunos casos límite.

6. Implementa las prácticas recomendadas de codificación

Tu calculadora de propinas funciona ahora, pero puedes mejorar un poco el código y facilitar el trabajo en el futuro si adoptas las prácticas recomendadas de codificación.

  1. Abre MainActivity.kt (app > java > com.example.tiptime > MainActivity).
  2. Observa el comienzo del métodocalculateTip(), y es posible que veas que está subrayado con una línea ondulada gris.

3737ebab72be9a5b.png

  1. Coloca el cursor sobre calculateTip() y verás el mensaje Function ‘calculateTip' could be private seguido de la siguiente sugerencia: Make ‘calculateTip' ‘private'. 6205e927b4c14cf3.png

Recuerda de los codelabs anteriores que private indica que el método o la variable solo es visible para el código dentro de esa clase, en este caso, MainActivity. No hay razones para que el código fuera de MainActivity llame a calculateTip(), por lo que puedes establecerlo como private sin problemas.

  1. Elige Make ‘calculateTip' ‘private' o agrega la palabra clave private antes de fun calculateTip(). La línea gris debajo de calculateTip() desaparece.

Cómo inspeccionar el código

La línea gris es muy sutil y fácil de ignorar. Puedes analizar el archivo completo para buscar más líneas grises, pero existe una manera más sencilla de asegurarse de encontrar todas las sugerencias.

  1. Con el elemento MainActivity.kt todavía abierto, selecciona Analyze > Inspect Code… en los menús. Aparecerá un cuadro de diálogo llamado Specify Inspection Scope. 1d2c6f8415e96231.png
  2. Elige la opción que comienza con File y presiona OK. De esa manera, se limitará la inspección a solo MainActivity.kt.
  3. Aparecerá una ventana con Inspection Results en la parte inferior.
  4. Haz clic en los triángulos grises junto a Kotlin y, luego, junto a Style issues hasta que veas dos mensajes. El primero es este: Class member can have ‘private' visibility. e40a6876f939c0d9.png
  5. Haz clic en los triángulos grises hasta que aparezca el mensaje Property ‘binding' could be private y haz clic en el mensaje. Android Studio muestra parte del código en MainActivity y destaca la variable binding. 8d9d7b5fc7ac5332.png
  6. Presiona el botón Make ‘binding' ‘private'. Android Studio quita el problema de Inspection Results.
  7. Si observas binding en tu código, verás que Android Studio agregó la palabra clave private antes de la declaración.
private lateinit var binding: ActivityMainBinding
  1. Haz clic en los triángulos grises en los resultados hasta que veas el mensaje Variable declaration could be inlined. Android Studio vuelve a mostrar parte del código, pero esta vez destaca la variable selectedId. 781017cbcada1194.png
  2. Si observas el código, verás que selectedId solo se usa dos veces: primero, en la línea destacada donde se asignó el valor de tipOptions.checkedRadioButtonId; luego, en la línea siguiente, en el elemento when.
  3. Presiona el botón Inline variable. Android Studio reemplaza selectedId en la expresión when con el valor que se asignó en la línea anterior. Luego, quita por completo la línea anterior, porque ya no es necesaria.
val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

Eso es muy práctico. El código tiene una línea menos y una variable menos.

Cómo quitar variables innecesarias

Android Studio no tiene más resultados de la inspección. Sin embargo, si observas el código con cuidado, verás un patrón similar al que acabas de cambiar: la variable roundUp se asigna en una línea, se utiliza en la línea siguiente y no se usa en ningún otro lugar.

  1. Copia la expresión a la derecha del elemento = desde la línea en la que se asigna roundUp.
val roundUp = binding.roundUpSwitch.isChecked
  1. Reemplaza roundUp en la línea siguiente con la expresión que acabas de copiar, binding.roundUpSwitch.isChecked.
if (binding.roundUpSwitch.isChecked) {
    tip = kotlin.math.ceil(tip)
}
  1. Borra la línea con roundUp, porque ya no es necesaria.

Hiciste lo mismo que Android Studio te ayudó a hacer con la variable selectedId. Nuevamente, tu código tiene una línea menos y una variable menos. Son cambios pequeños, pero ayudan a que el código sea más conciso y legible.

Cómo borrar el código repetitivo (opcional)

Una vez que tu app se ejecute de manera correcta, podrás buscar otras oportunidades para limpiar el código y lograr que sea más conciso. Por ejemplo, cuando no introduces un valor en el costo, la app actualiza tipResult para que sea una cadena vacía "". Cuando hay un valor, usa NumberFormat para darle formato. Esta funcionalidad se puede aplicar en otras partes de la app, por ejemplo, para mostrar una propina de 0.0 en lugar de una cadena vacía.

Para reducir la duplicación de código muy similar, puedes extraer estas dos líneas del código a su propia función. Esta función auxiliar puede tomar como entrada un importe de propina como Double, darle formato y actualizar el elemento tipResult TextView en la pantalla.

  1. Identifica el código duplicado en MainActivity.kt. Estas líneas de código se pueden usar varias veces en la función calculateTip(), una vez para el caso 0.0 y otra para el caso general.
val formattedTip = NumberFormat.getCurrencyInstance().format(0.0)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
  1. Mueve el código duplicado a su propia función. Un cambio en el código es tomar una propina de parámetro para que el código funcione en varios lugares.
private fun displayTip(tip : Double) {
   val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
   binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
  1. Actualiza tu función calculateTip() para usar la función auxiliar displayTip() y buscar 0.0 también.

MainActivity.kt

private fun calculateTip() {
    ...

        // If the cost is null or 0, then display 0 tip and exit this function early.
        if (cost == null || cost == 0.0) {
            displayTip(0.0)
            return
        }

    ...
    if (binding.roundUpSwitch.isChecked) {
        tip = kotlin.math.ceil(tip)
    }

    // Display the formatted tip value on screen
    displayTip(tip)
}

Nota

Si bien la app está funcionando en este momento, aún no está lista para producción. Debes realizar más pruebas. Además, debes agregar algunas mejoras visuales y seguir los lineamientos de Material Design. En los siguientes codelabs, también aprenderás a cambiar el tema y el ícono de la app.

7. Código de solución

A continuación, encontrarás el código de la solución para este codelab.

966018df4a149822.png

MainActivity.kt

(Nota sobre la primera línea: Reemplaza el nombre del paquete si el tuyo es diferente a com.example.tiptime).

package com.example.tiptime

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.tiptime.databinding.ActivityMainBinding
import java.text.NumberFormat

class MainActivity : AppCompatActivity() {

   private lateinit var binding: ActivityMainBinding

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       binding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(binding.root)

       binding.calculateButton.setOnClickListener { calculateTip() }
   }

   private fun calculateTip() {
       val stringInTextField = binding.costOfService.text.toString()
       val cost = stringInTextField.toDoubleOrNull()
       if (cost == null) {
           binding.tipResult.text = ""
           return
       }

       val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
           R.id.option_twenty_percent -> 0.20
           R.id.option_eighteen_percent -> 0.18
           else -> 0.15
       }

       var tip = tipPercentage * cost
       if (binding.roundUpSwitch.isChecked) {
           tip = kotlin.math.ceil(tip)
       }

       val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
       binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
   }
}

Modifica strings.xml

<string name="tip_amount">Tip Amount: %s</string>

Modifica activity_main.xml

...

<TextView
   android:id="@+id/tip_result"
   ...
   tools:text="Tip Amount: $10" />

...

Modifica el objeto build.gradle del módulo de la app.

android {
    ...

    buildFeatures {
        viewBinding = true
    }
    ...
}

8. Resumen

  • La vinculación de vista te permite escribir más fácilmente código que interactúe con los elementos de la IU en tu app.
  • El tipo de datos Double en Kotlin puede almacenar un número decimal.
  • Usa el atributo checkedRadioButtonId de un elemento RadioGroup para buscar qué RadioButton se seleccionó.
  • Usa NumberFormat.getCurrencyInstance() para obtener un formateador a fin de darles formato de moneda a los números.
  • Puedes usar parámetros de cadenas, como %s para crear cadenas dinámicas que todavía puedan traducirse fácilmente a otros idiomas.
  • ¡Las pruebas son importantes!
  • En Android Studio, puedes usar Logcat para solucionar problemas, por ejemplo, cuando falla la app.
  • Un seguimiento de pila muestra una lista de los métodos que se llamaron, lo que puede ser útil si el código genera una excepción.
  • Las excepciones indican un problema que el código no esperaba.
  • Null significa "sin valor".
  • No todo el código puede controlar los valores null, así que ten cuidado cuando los uses.
  • Usa Analyze > Inspect Code para obtener sugerencias que te permitan mejorar tu código.

9. Más codelabs para mejorar tu IU

¡Felicitaciones por lograr que la calculadora de propinas funcione! Notarás que todavía existen maneras de perfeccionar la IU para que la app luzca mejor. Si te interesa, consulta estos codelabs adicionales para obtener información para cambiar el tema de la app y el ícono de la app, y para seguir las prácticas recomendadas de los lineamientos de Material Design para la app de Tip Time.

10. Más información

11. Practica por tu cuenta

  • Con la app de conversión de unidades para cocina de la práctica anterior, agrega código para la lógica y los cálculos a fin de convertir unidades, por ejemplo, de mililitros a onzas líquidas, o viceversa.