1. 准备工作
在此文章中,您将为要与在上一个文章“为 Android 创建 XML 布局”中创建的界面搭配使用的小费计算器编写代码。
前提条件
- 为 Android 创建 XML
布局文章中的代码。 - 了解如何在模拟器中或在设备上从 Android Studio 运行 Android 应用。
学习内容
- Android 应用的基本结构。
- 如何从界面读取值到代码中并操纵它们。
- 如何使用视图绑定而非
findViewById()
来更轻松地编写与视图交互的代码。 - 如何将 Kotlin 中的十进制数字与
Double
数据类型搭配使用。 - 如何将数字的格式设置为货币。
- 如何使用字符串参数动态创建字符串。
- 如何使用 Android Studio 中的 Logcat 找出应用中的问题。
构建内容
- 一个小费计算器应用,它带有一个可以正常工作的 Calculate 按钮。
所需条件 - 一台安装了最新稳定版 Android Studio 的计算机。
- Tip Time 应用的起始代码,其中包含小费计算器的布局。
2. 起始应用概览
上一篇文章中的 Tip Time 应用具有小费计算器需要的所有界面,但没有用于计算小费的代码。有一个 Calculate
按钮,但它还不能正常使用。Cost of Service EditText
可让用户输入服务费用。RadioButtons 列表可让用户选择小费百分比,Switch
可让用户选择是否应将小费向上舍入。小费金额显示在 TextView
中,最终 Calculate Button
将告知应用从其他字段中获取数据并计算小费金额。此文章 就是从这里接着来讲。
应用项目结构
IDE 中的应用项目由许多部分组成,包括 Kotlin 代码、XML 布局以及其他资源,如字符串和图片。在对应用进行更改之前,最好先熟悉一下环境。
- 在 Android Studio 中打开 Tip Time 项目。
- 如果未显示 Project 窗口,请选择 Android Studio 左侧的 Project 标签页。
- 从下拉列表中选择 Android 视图(如果尚未选择该视图)。
- Kotlin 文件(或 Java 文件)的 java 文件夹
MainActivity
- 小费计算器逻辑的所有 Kotlin 代码所在的类- 应用资源的 res 文件夹
activity_main.xml
- Android 应用的布局文件strings.xml
- 包含 Android 应用的字符串资源- Gradle Scripts 文件夹
Gradle 是 Android Studio 使用的自动化构建系统。每当您更改代码、添加资源或对应用进行其他更改时,Gradle 都会弄清楚更改了哪些内容,并执行必要的步骤来重建应用。它还会将应用安装在模拟器中或实体设备上并控制其执行。
构建应用时还涉及到其他文件夹和文件,但这些是您在此 Codelab 以及接下来的 Codelab 中将会使用的主要文件夹和文件。
注意:您也可以使用 Java 编程语言编写 Android 应用。
3. 视图绑定
为了计算小费,代码需要访问所有界面元素以读取用户的输入。您可以回想一下,在之前的 文章中讲过,代码需要先找到对 View
(如 Button
或 TextView
)的引用,然后才能对 View
调用方法或访问其属性。Android 框架提供了 findViewById()
方法,其用途正是您所需要的,那就是给定 View
的 ID,返回对它的引用。此方法行得通,但随着您向应用添加更多视图并且界面变得越来越复杂,使用 findViewById()
可能会变得很麻烦。
为了方便起见,Android 还提供了一项名为视图绑定的功能。只需提前多做一点工作,视图绑定就能使得在界面中对视图调用方法更加简便快捷。您需要在 Gradle 中为应用启用视图绑定,并对代码进行一些更改。
启用视图绑定
- 打开应用的
build.gradle
文件 (Gradle Scripts > build.gradle (Module: Tip_Time.app))。 - 在
android
部分中,添加以下代码行:
buildFeatures {
viewBinding = true
}
- 注意以下消息:Gradle files have changed since last project sync。
- 按 Sync Now。
片刻之后,您应该会看到 Android Studio 窗口底部显示以下消息:Gradle sync finished。如果需要,您可以关闭 build.gradle
文件。
初始化绑定对象
在之前的 文章中,您看到过 MainActivity
类中的 onCreate()
方法。它是应用启动并初始化 MainActivity
时最先调用的内容之一。您将创建并初始化一次绑定对象,而不是在应用中针对每个 View
调用 findViewById()
。
- 打开
MainActivity.kt
(app > java > com.example.tiptime > MainActivity)。 - 将
MainActivity
类的所有现有代码替换为以下代码,设置MainActivity
以使用视图绑定:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
- 以下代码行在类中为绑定对象声明一个顶级变量。之所以在此级别上定义该变量,是因为将在
MainActivity
类的多个方法中使用它。
lateinit var binding: ActivityMainBinding
lateinit
关键字是新内容。应保证代码在使用变量之前先对其进行初始化。如果不这样做,应用会崩溃。
- 以下代码行初始化
binding
对象,您将使用该对象访问activity_main.xml
布局中的Views
。
binding = ActivityMainBinding.inflate(layoutInflater)
- 设置 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"
这样是不是很棒!
注意:系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为 Pascal 大小写形式,并在末尾添加“Binding”一词。同样,系统会通过以下方式生成每个视图的引用:移除下划线并将视图名称转换为 camel 小写形式。例如,对于 Pascal 大小写形式,
activity_main.xml
会变为ActivityMainBinding
,并且您能以binding.textView
的形式访问@id/text_view
。
4. 计算小费
计算小费从用户点按 Calculate 按钮开始。这涉及到检查界面以查看服务费用是多少以及用户想要给的小费百分比。使用此信息,您可以计算服务费总额,并显示小费金额。
向按钮添加点击监听器
第一步是添加点击监听器,以指定 Calculate 按钮在用户点按它时应该做什么。
- 在
MainActivity.kt
的onCreate()
中,调用setContentView()
之后,在Calculate
按钮上设置点击监听器并让其调用calculateTip()
。
binding.calculateButton.setOnClickListener{ calculateTip() }
- 仍在
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
中,但您需要它作为一个数字,这样才能在计算中使用它。您可能还记得其他 文章中讲过的 Int
类型,但 Int
只能保存整数。如需在应用中使用十进制数字,请使用名为 Double
的数据类型,而不是 Int
。您可以在相应的文档中详细了解 Kotlin中的数字数据类型。Kotlin 提供了一种用于将 String
转换为 Double
的方法(名为 toDouble()
)。
- 首先,获取服务费用的文本。在
calculateTip()
方法中,获取 Cost of ServiceEditText
的文本属性,并将其赋值给一个名为stringInTextField
的变量。请记住,您可以使用binding
对象访问界面元素,还可以根据界面元素的资源 ID 名称(采用驼峰式大小写)引用界面元素。
val stringInTextField = binding.costOfService.text
请注意末尾的 .text
。第一部分 binding.costOfService
引用服务费用的界面元素。在末尾添加 .text
表示要获取该结果(EditText
对象),并从其获取 text
属性。这称为“链”,是 Kotlin 中的一种很常见的模式。
- 接下来,将文本转换为十进制数字。对
stringInTextField
调用toDouble()
,并将其存储在一个名为cost
的变量中。
val cost = stringInTextField.toDouble()
不过,这样行不通,需要对 String
调用 toDouble()
。原来 EditText
的 text
属性是 Editable
,因为它表示可以更改的文本。幸好,您可以通过对 Editable
调用 toString(
) 来将其转换为 String
。
- 对
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()
}
获取小费百分比
到目前为止,您已经有了服务费用。现在,您需要用户从 RadioButtons
的 RadioGroup
中选择的小费百分比。
- 在
calculateTip()
中,获取tipOptions
RadioGroup
的checkedRadioButtonId
属性,并将其赋值给一个名为selectedId
的变量。
val selectedId = binding.tipOptions.checkedRadioButtonId
现在,您知道选择了哪个 RadioButton
(R.id.option_twenty_percent
、R.id.option_eighteen_percent
和 R.id.fifteen_percent
其中之一),但还需要相应的百分比。您可以编写一系列 if/else
语句,但使用 when
表达式要简单得多。
- 添加以下代码行以获取小费百分比。
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
}
}
计算小费并将其向上舍入
现在,您已经有了服务费用和小费百分比,计算小费很简单:小费是服务费用乘以小费百分比,即小费 = 服务费用 * 小费百分比。(可选)该值可以向上舍入。
- 在
calculateTip()
中,在您已添加的其他代码后面,用tipPercentage
乘以cost
,并将其赋值给一个名为tip
的变量。
var tip = tipPercentage * cost
请注意,此处使用的是 var
,而不是 val
。这是因为,如果用户选择了相应的选项,您可能需要将值向上舍入,因此值可能会更改。
对于 Switch
元素,您可以检查 isChecked
属性,看看开关是否已“开启”。
- 将向上舍入开关的
isChecked
属性赋值给一个名为roundUp
的变量。
val roundUp = binding.roundUpSwitch.isChecked
“舍入”一词是指将十进制数字向上或向下调整到最接近的整数值,但在本例中,您只希望向上舍入或找到上限。为此,您可以使用 ceil()
函数。有几个采用该名称的函数,但您想要的是在 kotlin.math
中定义的一个函数。您可以添加 import
语句,但在本例中,更简单的做法是使用 kotlin.math.ceil(
) 告知 Android Studio 您指的是哪一个函数。
如果您想要使用几个数学函数,则添加 import
语句会更容易。
- 添加
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。
- 在
calculateTip()
中,在其他代码后面,调用NumberFormat.getCurrencyInstance()
。
NumberFormat.getCurrencyInstance()
这样为您提供了数字格式设置工具,您可以使用它将数字的格式设置为货币。
- 使用数字格式设置工具,将对
format()
方法的调用与tip
链接起来,并将结果赋值给一个名为formattedTip
的变量。
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
-
请注意,
NumberFormat
显示为红色。这是因为,Android Studio 无法自动弄清楚您指的是哪一个版本的NumberFormat
。 -
将指针悬停在
NumberFormat
上,然后在出现的弹出式窗口中选择 Import。
-
在可能的导入内容列表中,选择 NumberFormat (java.text)。Android Studio 会在
MainActivity
文件顶部添加import
语句,并且NumberFormat
不再显示为红色。
显示小费
现在是时候在应用的小费金额 TextView
元素中显示小费了。您可以只将 formattedTip
赋值给 text
属性,但最好标记金额表示什么。在使用汉语的中国,您可以将其显示为 Tip Amount: ¥12.34,(因为鄙人能力有限,只能根据官方文档设置成美元,后续掌握后会及时更改)但在其他语言中,数字可能需要出现在字符串的开头甚至中间。Android 框架为此提供了一种称为“字符串参数”的机制,这样翻译应用的人员就可以根据需要更改数字出现的位置。
- 打开
strings.xml
(app > res > values > strings.xml)。 - 将
tip_amount
字符串从Tip Amount
更改为Tip Amount: %s
。
<string name="tip_amount">Tip Amount: %s</string>
将在 %s
处插入设置了格式的货币。
- 现在,设置
tipResult
的文本。回到MainActivity.kt
的calculateTip()
方法中,调用getString(R.string.tip_amount, formattedTip)
并将其赋值给小费结果TextView
的text
属性。
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 设置占位符很有用。
- 打开
activity_main.xml
(app > res > layout > activity_main.xml)。 - 查找
tip_result
TextView
。 - 移除包含
android:text
属性的代码行。
android:text="@string/tip_amount"
- 为设置为
Tip Amount: $10
的tools:text
属性添加一个代码行。
tools:text="Tip Amount: $10"
由于这只是一个占位符,因此您不需要将字符串提取到资源中。运行应用时,它不会显示。
请注意,工具文本显示在布局编辑器中。
运行应用。输入服务费用的金额并选择一些选项,然后按 Calculate 按钮。
恭喜您 ,它可以正常使用!如果您未获得正确的小费金额,请返回本部分的第 1 步,确保您已做出所有必要的代码更改。
5. 测试和调试
您已经在不同的步骤中运行了应用,确保应用能按预期运行,但现在是时候进行一些额外的测试了。
现在,考虑一下在 calculateTip()
方法中信息是如何在应用中移动的,以及每一步可能会出现什么问题。
例如,在以下代码行中:
val cost = stringInTextField.toDouble()
如果 stringInTextField
不表示数字,会发生什么情况?如果用户未输入任何文本因而 stringInTextField
为空,会发生什么情况?
- 在模拟器中运行应用,但使用 Run > Debug ‘app’,而不是使用 Run > Run ‘app’。
- 尝试服务费用、小费金额以及是否向上舍入小费的不同组合,验证您在各种情况下点按 Calculate 时是否能够获得预期结果。
- 现在,尝试删除
Cost of Service
字段中的所有文本,然后点按Calculate
。糟糕,程序崩溃了。
调试崩溃问题
处理错误的第一步是弄清楚发生了什么。Android Studio 将系统中发生的情况保存在日志中,您可以通过日志弄清楚出了什么问题。
- 按 Android Studio 底部的 Logcat 按钮,或在菜单中依次选择 View > Tool Windows > Logcat。
- Logcat 窗口将显示在 Android Studio 底部,其中填充有一些看起来很奇怪的文本。
这些文本是一个堆栈轨迹,列出了发生崩溃时正在调用的方法。 - 在 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)
- 向下读,直到您找到包含
NumberFormatException
的行。
java.lang.NumberFormatException: empty String
右侧显示的是 empty String
。异常的类型告知您它与数字格式有关,其余部分告知您问题的基本信息:找到了一个空 String
,而它本应是一个具有值的 String
。
- 继续向下读,您会看到对
parseDouble()
的一些调用。 - 在这些调用下面,找到包含
calculateTip
的行。请注意,它也包含MainActivity
类。
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
- 仔细查看该行,您可以看到在代码中进行调用的确切位置,那就是
MainActivity.kt
中的第 22 行。(如果您输入的代码不同,此处可能是一个不同的数字。)该行会将String
转换为Double
,并将结果赋值给cost
变量。
val cost = stringInTextField.toDouble()
- 在 Kotlin 文档中查找用于处理
String
的toDouble()
方法。该方法称为String.toDouble()
。 - 页面上显示“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.0
的 Double
或包含零个字符串的空 String
“” 不同。Null
表示没有值,没有 Double
或没有 String
。许多方法都期望有一个值,但它们可能不知道如何处理 null
并且会停止,这意味着应用会崩溃,因此 Kotlin 试图限制使用 null
的位置。您将在未来的课程中详细了解这一点。
应用可以检查是否从 toDoubleOrNull()
返回 null
,并在返回 null
时以不同的方式处理,这样应用就不会崩溃。
- 在
calculateTip()
中,更改声明cost
变量的代码行以调用toDoubleOrNull()
,而不是调用toDouble()
。
val cost = stringInTextField.toDoubleOrNull()
- 在该行后面,添加一个语句来检查
cost
是否为null
,如果是,则从方法返回。return
指令表示退出方法而不执行其余指令。如果方法需要返回一个值,您可以使用带有表达式的return
指令来指定它。
if (cost == null) {
return
}
- 再次运行应用。
- 在 Cost of Service 字段中没有文本的情况下,点按 Calculate。这次应用没有崩溃!太棒了 - 您找到并修复了错误!
处理其他情况
并非所有错误都会导致应用崩溃,有时结果可能会让用户感到困惑。
下面是要考虑的其他情况。如果用户执行以下操作,会发生什么:
- 为服务费用输入有效的金额
- 点按 Calculate 以计算小费
- 删除服务费用
- 再次点按 Calculate
第一次,将按预期计算并显示小费。第二次,由于您刚刚添加的检查,calculateTip()
方法将提前返回,但应用仍显示之前的小费金额。这可能会让用户感到困惑,因此应添加一些代码,以便在出现问题时清除小费金额。
- 确认此问题就是发生的情况,具体方法是输入有效的服务费用并点按 Calculate,然后删除文本,并再次点按 Calculate。应该仍显示第一次的小费值。
- 在刚刚添加的
if
内,在return
语句前面,添加一个代码行以将tipResult
的text
属性设置为一个空字符串。
if (cost == null) {
binding.tipResult.text = ""
return
}
这样将在从 calculateTip()
返回之前清除小费金额。
- 再次运行应用,并尝试上述情况。当您第二次点按 Calculate 时,第一次的小费金额应该会消失。
恭喜!您已经创建了一个可以正常使用的 Android 小费计算器应用,并处理了一些极端情况!
6. 采用规范的编码做法
小费计算器现在已经可以正常使用,但您可以通过采用良好的编码做法,让代码变得更好一点,使其在将来更容易使用。
- 打开
MainActivity.kt
(app > java > com.example.tiptime > MainActivity)。 - 看一下
calculateTip()
方法的开头,您可能会看到,它带有一条波浪形的灰色下划线。
- 将指针悬停在
calculateTip()
上,您会看到一条消息:Function ‘calculateTip’ could be private,下面还有一条建议:Make ‘calculateTip’ ‘private’。
您可以回想一下,在之前的 文章中讲过,private
表示方法或变量只对该类(在本例中为MainActivity
类)中的代码可见。MainActivity
之外的代码没有理由调用calculateTip()
,因此您可以放心地将其设为private
。 - 选择 Make ‘calculateTip’ ‘private’,或者在 fun calculateTip() 前面添加 private 关键字。calculateTip() 下面的灰线消失了。
检查代码
灰线非常细,很容易被忽视。您可以在整个文件中查找更多灰线,但有一种更简单的方法可以确保您找到所有建议。
- 在
MainActivity.kt
仍打开的情况下,在菜单中依次选择 Analyze > Inspect Code…。此时将显示一个名为 Specify Inspection Scope 的对话框。
- 选择以 File 开头的选项,然后按 OK。这样会将检查范围限制为只有
MainActivity.kt
。 - 此时将在底部显示一个包含 Inspection Results 的窗口。
- 点击 Kotlin 旁边的灰色三角形,然后点击 Style issues 旁边的灰色三角形,直到您看到两条消息。第一条消息内容是 Class member can have ‘private’ visibility。
- 点击灰色三角形,直到您看到以下消息:Property ‘binding’ could be private,然后点击该消息。Android Studio 会显示
MainActivity
中的一些代码,并突出显示binding
变量。
- 按 Make ‘binding’ ‘private’ 按钮。Android Studio 会从 Inspection Results 中移除问题(如果还显示这个问题,那么就点击上边第四条图片中左上角的双绿色的三角形重新检查一遍即可)。
- 如果您看一下代码中的
binding
,您会看到 Android Studio 在声明前面添加了private
关键字。
private lateinit var binding: ActivityMainBinding
- 点击结果中的灰色三角形,直到您看到以下消息:Variable declaration could be inlined。Android Studio 会再次显示一些代码,但这次突出显示
selectedId
变量。
- 如果您看一下代码,您会看到
selectedId
只使用了两次:第一次是在突出显示的行中,在此处为其赋予了值tipOptions.checkedRadioButtonId
,第二次是在下一行的when
中。 - 按 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
变量在一行中赋值,在下一行中使用,在其他任何位置都不使用。
- 从
roundUp
赋值的行复制=
右侧的表达式。
val roundUp = binding.roundUpSwitch.isChecked
- 将下一行中的
roundUp
替换为您刚刚复制的表达式binding.roundUpSwitch.isChecked
。
if (binding.roundUpSwitch.isChecked) {
tip = kotlin.math.ceil(tip)
}
- 删除包含
roundUp
的行,因为不再需要这一行。
您执行的操作与 Android Studio 帮助您对 selectedId
变量执行的操作相同。同样,代码少了一行,少了一个变量。这些是细微的更改,但有助于提高代码的简洁性和可读性。
(可选)消除重复代码
一旦应用正确运行,您就可以寻找其他机会来清理代码,使其更简洁。例如,当您不在服务费用中输入值时,应用会将 tipResult
更新为一个空字符串 ""
。当存在值时,您可以使用 NumberFormat
设置其格式。此功能可以在应用中的其他位置使用,例如,显示小费 0.0
而不是空字符串。
为了减少非常相似的代码的重复,您可以将这两行代码提取到它们自己的函数中。此辅助函数可以将 Double
形式的小费金额作为输入、设置其格式,并更新屏幕上的 tipResult
TextView
。
- 识别
MainActivity.kt
中的重复代码。这些代码行可以在calculateTip()
函数中使用多次,一次用于0.0
的情况,一次用于一般情况。
val formattedTip = NumberFormat.getCurrencyInstance().format(0.0)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
- 将重复代码移至其自己的函数。对代码的一项更改是采用小费参数,这样代码就可以在多个位置使用。
private fun displayTip(tip : Double) {
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
- 更新
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 准则。您还会在接下来的文章中学习如何更改应用主题和应用图标。
7. 解决方案代码
此文章的解决方案代码如下所示。
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 数据类型可存储十进制数字。
- 使用
RadioGroup
的checkedRadioButtonId
属性查找选中了哪个RadioButton
。 - 使用
NumberFormat.getCurrencyInstance()
获取用于将数字格式设置为货币的格式设置工具。 - 您可以使用
%s
等字符串参数来创建动态字符串,这些字符串仍可以轻松地翻译成其他语言。 - 测试非常重要!
- 您可以使用 Android Studio 中的
Logcat
排查应用崩溃等问题。 - 堆栈轨迹显示了调用的方法列表。如果代码生成异常,这会很有用。
- 异常表示代码没有预料到的问题。
Null
表示“无值”。- 并非所有代码都可以处理
null
值,因此使用它时应格外小心。 - 依次选择 Analyze > Inspect Code,获取关于改进代码的建议。
9. 更多用于改进界面的 Codelab
您已经能让小费计算器正常工作,太棒了!您会注意到,仍有一些方法可以改进界面,让应用看起来更精美。如果您感兴趣,可以查看下面这些额外的 文章,以详细了解如何更改应用主题和应用图标,以及如何遵循 Tip Time 应用的 Material Design 准则中的最佳做法!
10. 了解详情
- Kotlin 中的
Double
数据类型 - Kotlin 中的数字数据类型
- Kotlin 中的 null 安全性
- 应用清单
- View 绑定
NumberFormat.getCurrencyInstance()
- 字符串参数
- 测试
- Logcat
- 分析堆栈轨迹
11. 自行练习
注意:可视需要进行练习。您可以借此机会练习您在此 Codelab 中学到的知识。
- 使用上一个练习中的烹饪计量单位转换器应用,为用于在毫升和液量盎司之间来回转换等的逻辑和计算添加代码。