计算小费

1. 准备工作

在此 Codelab 中,您将编写小费计算器的代码,以便与上一个 Codelab“为 Android 应用创建 XML 布局”中创建的界面搭配使用。

前提条件

学习内容

  • Android 应用的基本结构。
  • 如何从界面读取值到代码中并操纵它们。
  • 如何使用视图绑定而非 findViewById() 来更轻松地编写与视图交互的代码。
  • 如何将 Kotlin 中的十进制数字与 Double 数据类型搭配使用。
  • 如何将数字的格式设置为货币。
  • 如何使用字符串参数动态创建字符串。
  • 如何使用 Android Studio 中的 Logcat 找出应用中的问题。

构建内容

  • 一个小费计算器应用,具备一个可以正常使用的 Calculate 按钮。

所需条件

  • 一台安装了最新稳定版 Android Studio 的计算机。
  • Tip Time 应用的起始代码,其中包含小费计算器的布局。

2. 起始应用概览

上一个 Codelab 中的 Tip Time 应用具有小费计算器需要的所有界面,但没有用于计算小费的代码。有一个 Calculate 按钮,但它还不能正常使用。Cost of Service EditText 可让用户输入服务费用。RadioButtons 列表可让用户选择小费百分比,Switch 可让用户选择是否应将小费向上舍入。小费金额显示在 TextView 中,最终 Calculate Button 将告知应用从其他字段中获取数据并计算小费金额。此 Codelab 就是从这里接着来讲。

ebf5c40d4e12d4c7.png

应用项目结构

IDE 中的应用项目由许多部分组成,包括 Kotlin 代码、XML 布局以及其他资源,如字符串和图片。在对应用进行更改之前,最好先熟悉一下环境。

  1. 在 Android Studio 中打开 Tip Time 项目。
  2. 如果未显示 Project 窗口,请选择 Android Studio 左侧的 Project 标签页。
  3. 从下拉列表中选择 Android 视图(如果尚未选择该视图)。

2a83e2b0aee106dd.png

  • Kotlin 文件(或 Java 文件)的 java 文件夹
  • MainActivity - 小费计算器逻辑的所有 Kotlin 代码所在的类
  • 用来放置应用资源的 res 文件夹
  • activity_main.xml - Android 应用的布局文件
  • strings.xml - 包含 Android 应用的字符串资源
  • Gradle Scripts 文件夹

Gradle 是 Android Studio 使用的自动化构建系统。每当您更改代码、添加资源或对应用进行其他更改时,Gradle 都会弄清楚更改了哪些内容,并执行必要的步骤来重建应用。它还会将应用安装在模拟器中或实体设备上并控制其执行。

构建应用时还涉及到其他文件夹和文件,但这些是您在此 Codelab 以及接下来的 Codelab 中将会使用的主要文件夹和文件。

3. 视图绑定

为了计算小费,代码需要访问所有界面元素以读取用户的输入。您可以回想一下,在之前的 Codelab 中讲过,代码需要先找到对 View(如 ButtonTextView)的引用,然后才能对 View 调用方法或访问其属性。Android 框架提供了 findViewById() 方法,其用途正是您所需要的,那就是在给定 View ID 的情况下,返回对它的引用。这是一种可行的处理方式,但随着您向应用添加更多视图并且界面变得越来越复杂,使用 findViewById() 可能会变得很麻烦。

为了方便起见,Android 还提供了一项名为视图绑定的功能。只需提前多做一点工作,视图绑定就能使得在界面中对视图调用方法更加简便快捷。您需要在 Gradle 中为应用启用视图绑定,并对代码进行一些更改。

启用视图绑定

  1. 打开应用的 build.gradle 文件 (Gradle Scripts > build.gradle (Module: Tip_Time.app))。
  2. android 部分中,添加以下代码行:
buildFeatures {
    viewBinding = true
}
  1. 注意以下消息:Gradle files have changed since last project sync
  2. Sync Now

349d99c67c2f40f1.png

片刻之后,您应该会看到 Android Studio 窗口底部显示以下消息:Gradle sync finished。如果需要,您可以关闭 build.gradle 文件。

初始化绑定对象

在之前的 Codelab 中,您看到过 MainActivity 类中的 onCreate() 方法。它是应用启动并初始化 MainActivity 时最先调用的内容之一。您将创建并初始化一次绑定对象,而不是在应用中针对每个 View 分别调用 findViewById()

674d243aa6f85b8b.png

  1. 打开 MainActivity.kt (app > java > com.example.tiptime > MainActivity)。
  2. MainActivity 类的所有现有代码替换为以下代码,进而设置 MainActivity 以使用视图绑定:
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}
  1. 以下代码行在类中为绑定对象声明一个顶级变量。之所以在此级别上定义该变量,是因为该变量将会在 MainActivity 类的多个方法中用到。
lateinit var binding: ActivityMainBinding

lateinit 关键字是新内容。应保证代码在使用变量之前先对其进行初始化。如果不这样做,应用会崩溃。

  1. 以下代码行会初始化 binding 对象,您将使用该对象访问 activity_main.xml 布局中的 Views
binding = ActivityMainBinding.inflate(layoutInflater)
  1. 设置 activity 的内容视图。以下代码行指定应用中视图层次结构的根 binding.root,而不是传递布局 R.layout.activity_main 的资源 ID。
setContentView(binding.root)

您可能还记得父视图和子视图的概念;根连接到所有这些视图。

现在,当您需要在应用中引用 View 时,您可以从 binding 对象获取它,而不是调用 findViewById()binding 对象可自动为应用中具有 ID 的每个 View 定义引用。使用视图绑定要简洁得多,通常您甚至不需要创建一个变量来保存 View 的引用,只需直接从绑定对象使用它即可。

// 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"

这样是不是很棒!

4. 计算小费

计算小费从用户点按 Calculate 按钮开始。这涉及到检查界面以查看服务费用是多少以及用户想要给的小费百分比。使用此信息,您可以计算服务费总额,并显示小费金额。

向按钮添加点击监听器

第一步是添加点击监听器,以指定 Calculate 按钮在用户点按它时应该做什么。

  1. MainActivity.ktonCreate() 中,调用 setContentView() 之后,在 Calculate 按钮上设置点击监听器并让其调用 calculateTip()
binding.calculateButton.setOnClickListener{ calculateTip() }
  1. 仍在 MainActivity 类中,但在 onCreate() 之外,添加一个名为 calculateTip() 的辅助方法。
fun calculateTip() {

}

您将在此处添加相应的代码以检查界面并计算小费。

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() {

    }
}

获取服务费用

为了计算小费,您首先需要的是服务费用。文本存储在 EditText 中,但您需要它作为一个数字,这样才能在计算中使用它。您可能还记得其他 Codelab 中讲过的 Int 类型,但 Int 只能保存整数。如需在应用中使用十进制数字,请使用名为 Double 的数据类型,而不是 Int。您可以在相应的文档中详细了解 Kotlin 中的数字数据类型。Kotlin 提供了一种用于将 String 转换为 Double 的方法(名为 toDouble())。

  1. 首先,获取服务费用的文本。在 calculateTip() 方法中,获取 Cost of Service EditText 的文本属性,并将其赋值给一个名为 stringInTextField 的变量。请记住,您可以使用 binding 对象访问界面元素,还可以根据界面元素的资源 ID 名称(采用驼峰命名法)引用界面元素。
val stringInTextField = binding.costOfService.text

请注意末尾的 .text。第一部分 binding.costOfService 引用服务费用的界面元素。在末尾添加 .text 表示要获取该结果(EditText 对象),并从其获取 text 属性。这称为“链”,是 Kotlin 中的一种很常见的模式。

  1. 接下来,将文本转换为十进制数字。对 stringInTextField 调用 toDouble(),并将其存储在一个名为 cost 的变量中。
val cost = stringInTextField.toDouble()

不过,这样行不通 - 需要对 String 调用 toDouble()。原来 EditTexttext 属性是 Editable,因为它表示可以更改的文本。幸好,您可以通过对 Editable 调用 toString() 来将其转换为 String

  1. binding.costOfService.text 调用 toString() 以将其转换为 String
val stringInTextField = binding.costOfService.text.toString()

现在,stringInTextField.toDouble() 将发挥作用。

此时,calculateTip() 方法应如下所示:

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

获取小费百分比

到目前为止,您已经有了服务费用。现在,您需要用户从 RadioButtonsRadioGroup 中选择的小费百分比。

  1. calculateTip() 中,获取 tipOptions RadioGroupcheckedRadioButtonId 属性,并将其赋值给一个名为 selectedId 的变量。
val selectedId = binding.tipOptions.checkedRadioButtonId

现在,您知道选择了哪个 RadioButtonR.id.option_twenty_percentR.id.option_eighteen_percentR.id.fifteen_percent 其中之一),但还需要相应的百分比。您可以编写一系列 if/else 语句,但使用 when 表达式要简单得多。

  1. 添加以下代码行以获取小费百分比。
val tipPercentage = when (selectedId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

此时,calculateTip() 方法应如下所示:

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

计算小费并将其向上舍入

现在,您已经有了服务费用和小费百分比,计算小费很简单:小费是服务费用乘以小费百分比,即小费 = 服务费用 * 小费百分比。(可选)该值可以向上舍入。

  1. calculateTip() 中,在您已添加的其他代码后面,用 tipPercentage 乘以 cost,并将其赋值给一个名为 tip 的变量。
var tip = tipPercentage * cost

请注意,此处使用的是 var,而不是 val。这是因为,如果用户选择了相应的选项,您可能需要将值向上舍入,因此值可能会更改。

对于 Switch 元素,您可以检查 isChecked 属性,看看开关是否已“开启”。

  1. 将向上舍入开关的 isChecked 属性赋值给一个名为 roundUp 的变量。
val roundUp = binding.roundUpSwitch.isChecked

“舍入”一词是指将十进制数字向上或向下调整到最接近的整数值,但在本例中,您只希望向上舍入或找到上限。为此,您可以使用 ceil() 函数。有几个采用该名称的函数,但您想要的是在 kotlin.math 中定义的一个函数。您可以添加 import 语句,但在本例中,更简单的做法是使用 kotlin.math.ceil() 告知 Android Studio 您指的是哪一个函数。

32c29f73a3f20f93.png

如果您想要使用几个数学函数,则添加 import 语句会更容易。

  1. 添加 if 语句,如果 roundUp 为 true,则将小费的上限赋值给 tip 变量。
if (roundUp) {
    tip = kotlin.math.ceil(tip)
}

此时,calculateTip() 方法应如下所示:

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)
    }
}

设置小费的格式

您的应用几乎可以正常使用了。您已经计算了小费,现在只需设置小费的格式并显示小费。

如您所料,Kotlin 提供了一些用于为不同类型的数字设置格式的方法。但小费金额略有不同 - 它表示货币值。不同的国家/地区使用不同的货币,并且在设置十进制数字的格式方面有不同的规则。例如,以美元表示时,1234.56 的格式将设置为 $1,234.56,但以欧元表示时,它的格式将设置为 €1.234,56。幸好,Android 框架提供了一些用于将数字的格式设置为货币的方法,因此您不需要知道所有的可能性。系统会根据语言以及用户在手机上选择的其他设置来自动设置货币的格式。您可以在 Android 开发者文档中详细了解 NumberFormat

  1. calculateTip() 中,在其他代码后面,调用 NumberFormat.getCurrencyInstance()
NumberFormat.getCurrencyInstance()

这样为您提供了数字格式设置工具,您可以使用它将数字的格式设置为货币。

  1. 使用数字格式设置工具,将对 format() 方法的调用与 tip 链接起来,并将结果赋值给一个名为 formattedTip 的变量。
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
  1. 请注意,NumberFormat 显示为红色。这是因为,Android Studio 无法自动弄清楚您指的是哪一个版本的 NumberFormat
  2. 将指针悬停在 NumberFormat 上,然后在出现的弹出式窗口中选择 Importd9d2f92d5ef01df6.png
  3. 在可能的导入内容列表中,选择 NumberFormat (java.text)。Android Studio 会在 MainActivity 文件顶部添加 import 语句,并且 NumberFormat 不再显示为红色。

显示小费

现在是时候在应用的小费金额 TextView 元素中显示小费了。您可以只将 formattedTip 赋值给 text 属性,但最好标记金额表示什么。在使用英语的美国,您可以将其显示为 Tip Amount: $12.34,但在其他语言中,数字可能需要出现在字符串的开头甚至中间。Android 框架为此提供了一种称为“字符串参数”的机制,这样翻译应用的人员就可以根据需要更改数字出现的位置。

  1. 打开 strings.xml (app > res > values > strings.xml)。
  2. tip_amount 字符串从 Tip Amount 更改为 Tip Amount: %s
<string name="tip_amount">Tip Amount: %s</string>

将在 %s 处插入设置了格式的货币。

  1. 现在,设置 tipResult 的文本。回到 MainActivity.ktcalculateTip() 方法中,调用 getString(R.string.tip_amount, formattedTip) 并将其赋值给小费结果 TextViewtext 属性。
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)

此时,calculateTip() 方法应如下所示:

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)
}

即将大功告成。开发应用(以及查看预览)时,为该 TextView 设置占位符很有用。

  1. 打开 activity_main.xml (app > res > layout > activity_main.xml)。
  2. 查找 tip_result TextView
  3. 移除包含 android:text 属性的代码行。
android:text="@string/tip_amount"
  1. 为设置为 Tip Amount: $10tools:text 属性添加一个代码行。
tools:text="Tip Amount: $10"

由于这只是一个占位符,因此您不需要将字符串提取到资源中。运行应用时,它不会显示。

  1. 请注意,工具文本显示在布局编辑器中。
  2. 运行应用。输入服务费用的金额并选择一些选项,然后按 Calculate 按钮。

42fd6cd5e24ca433.png

恭喜您 - 它可以正常使用!如果您未获得正确的小费金额,请返回本部分的第 1 步,确保您已做出所有必要的代码更改。

5. 测试和调试

您已经在不同的步骤中运行了应用,确保应用能按预期运行,但现在是时候进行一些额外的测试了。

现在,考虑一下在 calculateTip() 方法中信息是如何在应用中移动的,以及每一步可能会出现什么问题。

例如,在以下代码行中:

val cost = stringInTextField.toDouble()

如果 stringInTextField 不表示数字,会发生什么情况?如果用户未输入任何文本因而 stringInTextField 为空,会发生什么情况?

  1. 在模拟器中运行应用,但使用 Run > Debug ‘app',而不是使用 Run > Run ‘app'
  2. 尝试服务费用、小费金额以及是否向上舍入小费的不同组合,验证您在各种情况下点按 Calculate 时是否能够获得预期结果。
  3. 现在,尝试删除 Cost of Service 字段中的所有文本,然后点按 Calculate。糟糕,程序崩溃了。

调试崩溃问题

处理错误的第一步是弄清楚发生了什么。Android Studio 将系统中发生的情况保存在日志中,您可以通过日志弄清楚出了什么问题。

  1. 按 Android Studio 底部的 Logcat 按钮,或在菜单中依次选择 View > Tool Windows > Logcat

1b68ee5190018c8a.png

  1. Logcat 窗口将显示在 Android Studio 底部,其中填充有一些看起来很奇怪的文本。22139575476ae9d.png

这些文本是一个堆栈轨迹,列出了发生崩溃时正在调用的方法。

  1. Logcat 文本中向上滚动,直到找到包含 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. 向下读,直到您找到包含 NumberFormatException 的行。
java.lang.NumberFormatException: empty String

右侧显示的是 empty String。异常的类型告知您它与数字格式有关,其余部分告知您问题的基本信息:找到了一个空 String,而它本应是一个具有值的 String

  1. 继续向下读,您会看到对 parseDouble() 的一些调用。
  2. 在这些调用下面,找到包含 calculateTip 的行。请注意,它也包含 MainActivity 类。
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
  1. 仔细查看该行,您可以看到在代码中进行调用的确切位置,那就是 MainActivity.kt 中的第 22 行。(如果您输入的代码不同,此处可能是一个不同的数字。)该行会将 String 转换为 Double,并将结果赋值给 cost 变量。
val cost = stringInTextField.toDouble()
  1. 在 Kotlin 文档中查找用于处理 StringtoDouble() 方法。该方法称为 String.toDouble()
  2. 页面上显示“Exceptions: NumberFormatException - if the string is not a valid representation of a number”。

异常是系统提示出现问题的一种方式。在本例中,问题是 toDouble() 无法将空 String 转换为 Double。虽然 EditText 具有 inputType=numberDecimal 设置,但仍有可能输入一些 toDouble() 无法处理的值,如一个空字符串。

了解 null

对空字符串或不表示有效十进制数字的字符串调用 toDouble() 不起作用。幸好,Kotlin 还提供了一种名为 toDoubleOrNull() 的方法来处理这些问题。如果可以,它会返回十进制数字;如果存在问题,它会返回 null

null 是一个特殊值,表示“无值”。它与值为 0.0Double 或包含零个字符串的空 String "" 不同。Null 表示没有值,没有 Double 或没有 String。许多方法都期望有一个值,但它们可能不知道如何处理 null 并且会停止,这意味着应用会崩溃,因此 Kotlin 试图限制使用 null 的位置。您将在未来的课程中详细了解这一点。

应用可以检查是否从 toDoubleOrNull() 返回 null,并在返回 null 时以不同的方式处理,这样应用就不会崩溃。

  1. calculateTip() 中,更改声明 cost 变量的代码行以调用 toDoubleOrNull(),而不是调用 toDouble()
val cost = stringInTextField.toDoubleOrNull()
  1. 在该行后面,添加一个语句来检查 cost 是否为 null,如果是,则从方法返回。return 指令表示退出方法而不执行其余指令。如果方法需要返回一个值,您可以使用带有表达式的 return 指令来指定它。
if (cost == null) {
    return
}
  1. 再次运行应用。
  2. Cost of Service 字段中没有文本的情况下,点按 Calculate。这次应用没有崩溃!太棒了 - 您找到并修复了 bug!

处理其他情况

并非所有错误都会导致应用崩溃,有时结果可能会让用户感到困惑。

下面是要考虑的其他情况。如果用户执行以下操作,会发生什么:

  1. 为服务费用输入有效的金额
  2. 点按 Calculate 以计算小费
  3. 删除服务费用
  4. 再次点按 Calculate

第一次,将按预期计算并显示小费。第二次,由于您刚刚添加的检查,calculateTip() 方法将提前返回,但应用仍显示之前的小费金额。这可能会让用户感到困惑,因此应添加一些代码,以便在出现问题时清除小费金额。

  1. 确认此问题就是发生的情况,具体方法是输入有效的服务费用并点按 Calculate,然后删除文本,并再次点按 Calculate。应该仍显示第一次的小费值。
  2. 在刚刚添加的 if 内,在 return 语句前面,添加一个代码行以将 tipResulttext 属性设置为一个空字符串。
if (cost == null) {
    binding.tipResult.text = ""
    return
}

这样将在从 calculateTip() 返回之前清除小费金额。

  1. 再次运行应用,并尝试上述情况。当您第二次点按 Calculate 时,第一次的小费金额应该会消失。

恭喜!您已经创建了一个可以正常使用的 Android 小费计算器应用,并处理了一些极端情况!

6. 采用规范的编码做法

小费计算器现在已经可以正常使用,但您可以通过采用良好的编码做法,让代码变得更好一点,使其在将来更容易使用。

  1. 打开 MainActivity.kt (app > java > com.example.tiptime > MainActivity)。
  2. 看一下 calculateTip() 方法的开头,您可能会看到,它带有一条波浪形的灰色下划线。

3737ebab72be9a5b.png

  1. 将指针悬停在 calculateTip() 上,您会看到一条消息:Function ‘calculateTip' could be private,下面还有一条建议:Make ‘calculateTip' ‘private'6205e927b4c14cf3.png

您可以回想一下,在之前的 Codelab 中讲过,private 表示方法或变量只对该类(在本例中为 MainActivity 类)中的代码可见。MainActivity 之外的代码没有理由调用 calculateTip(),因此您可以放心地将其设为 private

  1. 选择 Make ‘calculateTip' ‘private',或者在 fun calculateTip() 前面添加 private 关键字。calculateTip() 下面的灰线消失了。

检查代码

灰线非常细,很容易被忽视。您可以在整个文件中查找更多灰线,但有一种更简单的方法可以确保您找到所有建议。

  1. MainActivity.kt 仍打开的情况下,在菜单中依次选择 Analyze > Inspect Code...。此时将显示一个名为 Specify Inspection Scope 的对话框。1d2c6f8415e96231.png
  2. 选择以 File 开头的选项,然后按 OK。这样会将检查范围限制为只有 MainActivity.kt
  3. 此时将在底部显示一个包含 Inspection Results 的窗口。
  4. 点击 Kotlin 旁边的灰色三角形,然后点击 Style issues 旁边的灰色三角形,直到您看到两条消息。第一条消息内容是 Class member can have ‘private' visibilitye40a6876f939c0d9.png
  5. 点击灰色三角形,直到您看到以下消息:Property ‘binding' could be private,然后点击该消息。Android Studio 会显示 MainActivity 中的一些代码,并突出显示 binding 变量。8d9d7b5fc7ac5332.png
  6. Make ‘binding' ‘private' 按钮。Android Studio 会从 Inspection Results 中移除问题。
  7. 如果您看一下代码中的 binding,您会看到 Android Studio 在声明前面添加了 private 关键字。
private lateinit var binding: ActivityMainBinding
  1. 点击结果中的灰色三角形,直到您看到以下消息:Variable declaration could be inlined。Android Studio 会再次显示一些代码,但这次突出显示 selectedId 变量。781017cbcada1194.png
  2. 如果您看一下代码,您会看到 selectedId 只使用了两次:第一次是在突出显示的行中,在此处为其赋予了值 tipOptions.checkedRadioButtonId,第二次是在下一行的 when 中。
  3. Inline variable 按钮。Android Studio 会将 when 表达式中的 selectedId 替换为前面一行中赋予的值。然后,它会完全移除前一行,因为不再需要这一行了!
val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

这真的是太棒了!代码少了一行,少了一个变量。

移除不必要的变量

Android Studio 不再有任何检查结果。不过,如果您仔细看一下代码,您会看到一个与刚刚更改的模式类似的模式:roundUp 变量在一行中赋值,在下一行中使用,在其他任何位置都不使用。

  1. roundUp 赋值的行复制 = 右侧的表达式。
val roundUp = binding.roundUpSwitch.isChecked
  1. 将下一行中的 roundUp 替换为您刚刚复制的表达式 binding.roundUpSwitch.isChecked
if (binding.roundUpSwitch.isChecked) {
    tip = kotlin.math.ceil(tip)
}
  1. 删除包含 roundUp 的行,因为不再需要这一行。

您执行的操作与 Android Studio 帮助您对 selectedId 变量执行的操作相同。同样,代码少了一行,少了一个变量。这些是细微的更改,但有助于提高代码的简洁性和可读性。

(可选)消除重复代码

一旦应用正确运行,您就可以寻找其他机会来清理代码,使其更简洁。例如,当您不在服务费用中输入值时,应用会将 tipResult 更新为一个空字符串 ""。当存在值时,您可以使用 NumberFormat 设置其格式。此功能可以在应用中的其他位置使用,例如,显示小费 0.0 而不是空字符串。

为了减少非常相似的代码的重复,您可以将这两行代码提取到它们自己的函数中。此辅助函数可以将 Double 形式的小费金额作为输入、设置其格式,并更新屏幕上的 tipResult TextView

  1. 识别 MainActivity.kt 中的重复代码。这些代码行可以在 calculateTip() 函数中使用多次,一次用于 0.0 的情况,一次用于一般情况。
val formattedTip = NumberFormat.getCurrencyInstance().format(0.0)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
  1. 将重复代码移至其自己的函数。对代码的一项更改是采用小费参数,这样代码就可以在多个位置使用。
private fun displayTip(tip : Double) {
   val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
   binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
  1. 更新 calculateTip() 函数以使用 displayTip() 辅助函数并检查 0.0

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)
}

备注

虽然应用已经可以正常运行,但还没有准备好投入使用。您需要进行更多测试。而且,您还需要从视觉上稍加润色,并遵循 Material Design 准则。您还会在接下来的 Codelab 中学习如何更改应用主题和应用图标。

7. 解决方案代码

此 Codelab 的解决方案代码如下所示。

966018df4a149822.png

MainActivity.kt

(请注意,在第一行:如果您的软件包名称与 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)
   }
}

修改 strings.xml

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

修改 activity_main.xml

...

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

...

修改应用模块的 build.gradle

android {
    ...

    buildFeatures {
        viewBinding = true
    }
    ...
}

8. 总结

  • 视图绑定可让您更轻松地编写可与应用中的界面元素交互的代码。
  • Kotlin 中的 Double 数据类型可存储十进制数字。
  • 使用 RadioGroupcheckedRadioButtonId 属性查找选中了哪个 RadioButton
  • 使用 NumberFormat.getCurrencyInstance() 获取用于将数字格式设置为货币的格式设置工具。
  • 您可以使用 %s 等字符串参数来创建动态字符串,这些字符串仍可以轻松地翻译成其他语言。
  • 测试非常重要!
  • 您可以使用 Android Studio 中的 Logcat 排查应用崩溃等问题。
  • 堆栈轨迹显示了调用的方法列表。如果代码生成异常,这会很有用。
  • 异常表示代码没有预料到的问题。
  • Null 表示“无值”。
  • 并非所有代码都可以处理 null 值,因此使用它时应格外小心。
  • 依次选择 Analyze > Inspect Code,获取关于改进代码的建议。

9. 更多用于改进界面的 Codelab

您已经能让小费计算器正常工作,太棒了!您会注意到,仍有一些方法可以改进界面,让应用看起来更精美。如果您感兴趣,可以查看下面这些额外的 Codelab,以详细了解如何更改应用主题和应用图标,以及如何遵循 Tip Time 应用的 Material Design 准则中的最佳实践!

10. 了解详情

11. 自行练习

  • 使用上一个练习中的烹饪计量单位转换器应用,为用于在毫升和液量盎司之间来回转换等的逻辑和计算添加代码。