1. 开始之前
此 Codelab 将教你了解状态,以及 Jetpack Compose 如何使用和操作状态。
从根本上说,应用中的状态是指任何可能随时间变化的值。此定义非常广泛,包括从数据库到应用中变量的所有内容。您将在后面的单元中详细了解数据库,但现在您只需要知道数据库是结构化信息的组织集合,例如计算机上的文件。
所有 Android 应用都向用户显示状态。Android 应用中状态的一些示例包括
- 建立网络连接失败时显示的消息。
- 表单,例如注册表单。状态可以填写并提交。
- 可点击的控件,例如按钮。状态可以是未点击、正在点击(显示动画)或已点击(
onClick
操作)。
在此 Codelab 中,您将探索在使用 Compose 时如何使用和思考状态。为此,您将构建一个名为 Tip Time 的计算器应用,其中包含以下内置的 Compose UI 元素
- 用于输入和编辑文本的
TextField
可组合函数。 - 用于显示文本的
Text
可组合函数。 - 用于在 UI 元素之间显示空白空间的
Spacer
可组合函数。
在本 Codelab 结束时,您将构建一个交互式小费计算器,当您输入服务金额时,它会自动计算小费金额。此图片显示了最终应用的外观
先决条件
- 对 Compose 的基本了解,例如
@Composable
注解。 - 对 Compose 布局的基本熟悉,例如
Row
和Column
布局可组合函数。 - 对修饰符的基本熟悉,例如
Modifier.padding()
函数。 - 熟悉
Text
可组合函数。
您将学到什么
- 如何在 UI 中思考状态。
- Compose 如何使用状态显示数据。
- 如何在应用中添加文本框。
- 如何提升状态。
您将构建什么
- 一个名为 Tip Time 的计算器应用,该应用根据服务金额计算小费金额。
您需要什么
- 一台连接互联网的电脑和网络浏览器
- Kotlin 知识
- 最新版本的 Android Studio
2. 开始使用
- 查看 Google 的在线计算器。请注意,这只是一个示例,这不是您将在本课程中创建的 Android 应用。
- 在“账单”和“小费 %”框中输入不同的值。小费和总计值会发生变化。
请注意,您输入值的那一刻,“小费”和“总计”就会更新。在本 Codelab 结束时,您将在 Android 中开发一个类似的计算器应用。
在此路径中,您将构建一个简单的计算器 Android 应用。
开发人员通常会以这种方式工作——准备好并运行应用的简单版本(即使它看起来不太好),然后在以后添加更多功能并使其更具视觉吸引力。
在本 Codelab 结束时,您的计算器应用将如下面的屏幕截图所示。当用户输入账单金额时,您的应用将显示建议的小费金额。现在,小费百分比硬编码为15%。在下一个 Codelab 中,您将继续开发应用并添加更多功能,例如设置自定义小费百分比。
3. 获取启动代码
启动代码是预先编写的代码,可用作新项目的起点。它还可以帮助您专注于本 Codelab 中教授的新概念。
通过在此处下载启动代码开始使用
或者,您可以克隆代码的 GitHub 存储库
$ 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 starter
您可以在 TipTime
GitHub 存储库中浏览启动代码。
启动应用概述
要熟悉启动代码,请完成以下步骤
- 在 Android Studio 中打开包含启动代码的项目。
- 在 Android 设备或模拟器上运行应用。
- 您将注意到两个文本组件;一个用于标签,另一个用于显示小费金额。
启动代码演练
启动代码包含文本可组合函数。在此路径中,您将添加一个文本字段以获取用户输入。以下是一些入门文件的简要演练。
res > values > strings.xml
<resources>
<string name="app_name">Tip Time</string>
<string name="calculate_tip">Calculate Tip</string>
<string name="bill_amount">Bill Amount</string>
<string name="tip_amount">Tip Amount: %s</string>
</resources>
这是资源中的 string.xml
文件,其中包含您将在此应用中使用的所有字符串。
MainActivity
此文件主要包含模板生成的代码和以下函数。
TipTimeLayout()
函数包含一个Column
元素,其中有两个您在屏幕截图中看到的文本可组合函数。它还具有spacer
可组合函数以出于美观原因添加空间。calculateTip()
函数接受账单金额并计算 15% 的小费金额。tipPercent
参数设置为15.0
默认参数值。这将当前默认小费值设置为 15%。在下一个 Codelab 中,您将从用户那里获取小费金额。
@Composable
fun TipTimeLayout() {
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
)
Text(
text = stringResource(R.string.tip_amount, "$0.00"),
style = MaterialTheme.typography.displaySmall
)
Spacer(modifier = Modifier.height(150.dp))
}
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
val tip = tipPercent / 100 * amount
return NumberFormat.getCurrencyInstance().format(tip)
}
在 onCreate()
函数的 Surface()
块中,正在调用 TipTimeLayout()
函数。这将在设备或模拟器中显示应用的布局。
override fun onCreate(savedInstanceState: Bundle?) {
//...
setContent {
TipTimeTheme {
Surface(
//...
) {
TipTimeLayout()
}
}
}
}
在 TipTimeLayoutPreview()
函数的 TipTimeTheme
块中,正在调用 TipTimeLayout()
函数。这将在“设计”和“拆分”窗格中显示应用的布局。
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
}
}
获取用户输入
在本部分中,您将添加允许用户在应用中输入账单金额的 UI 元素。您可以在此图片中看到它的外观
您的应用使用自定义样式和主题。
样式和主题是属性的集合,用于指定单个 UI 元素的外观。样式可以指定字体颜色、字体大小、背景颜色等属性,这些属性可以应用于整个应用。以后的 Codelab 将介绍如何在您的应用中实现这些属性。目前,这些属性已为您完成,以使您的应用更美观。
为了更好地理解,以下是应用解决方案版本在使用和不使用自定义主题时的并排比较。
无自定义主题。 | 使用自定义主题。 |
TextField
可组合函数允许用户在应用中输入文本。例如,请注意此图片中 Gmail 应用登录屏幕上的文本框
将 TextField
可组合函数添加到应用中
- 在
MainActivity.kt
文件中,添加一个EditNumberField()
可组合函数,该函数采用Modifier
参数。 - 在
EditNumberField()
函数体中,在TipTimeLayout()
之后添加一个TextField
,该函数接受名为value
的参数并将其设置为一个空字符串,以及名为onValueChange
的参数并将其设置为一个空 lambda 表达式
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
- 请注意您传递的参数
value
参数是一个文本框,它显示您在此处传递的字符串值。onValueChange
参数是当用户在文本框中输入文本时触发的 lambda 回调。
- 导入此函数
import androidx.compose.material3.TextField
- 在
TipTimeLayout()
可组合函数中,在第一个文本可组合函数之后的行中,调用EditNumberField()
函数,并传递以下修饰符。
import androidx.compose.foundation.layout.fillMaxWidth
@Composable
fun TipTimeLayout() {
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
...
)
EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
Text(
...
)
...
}
}
这将在屏幕上显示文本框。
- 在“设计”窗格中,您应该会看到“计算小费”文本、一个空文本框和“小费金额”文本可组合函数。
4. 在 Compose 中使用状态
应用中的状态是指任何可能随时间变化的值。在此应用中,状态是账单金额。
添加一个变量来存储状态
- 在
EditNumberField()
函数的开头,使用val
关键字添加一个名为amountInput
的变量,并将其设置为"0"
值
val amountInput = "0"
这是应用的账单金额状态。
- 将名为
value
的参数设置为amountInput
值
TextField(
value = amountInput,
onValueChange = {},
)
- 检查预览。文本框显示设置为状态变量的值,如您在此图片中看到的
- 在模拟器中运行应用,尝试输入不同的值。硬编码的状态保持不变,因为
TextField
可组合函数不会自行更新。当其value
参数更改时,它会更新,该参数设置为amountInput
属性。
amountInput
变量表示文本框的状态。硬编码状态没有用,因为它无法修改,并且不反映用户输入。当用户更新账单金额时,您需要更新应用的状态。
5. 组合
应用中的可组合函数描述了一个 UI,它显示一个包含一些文本、间隔符和文本框的列。文本显示 Calculate Tip
文本,文本框显示 0
值或任何默认值。
Compose 是一个声明式 UI 框架,这意味着您在代码中声明 UI 的外观。如果您希望文本框最初显示 100
值,则需要在可组合函数的代码中将初始值设置为 100
值。
如果希望 UI 在应用运行期间或用户与应用交互时发生变化该怎么办?例如,如果希望使用用户输入的值更新 amountInput
变量并在文本框中显示它该怎么办?这时,您需要依靠一个称为重新组合的过程来更新应用的组合。
组合是 Compose 在执行可组合函数时构建的 UI 的描述。Compose 应用调用可组合函数将数据转换为 UI。如果发生状态更改,Compose 会使用新状态重新执行受影响的可组合函数,从而创建更新的 UI——这称为重新组合。Compose 会为您安排重新组合。
当 Compose 在初始组合期间首次运行您的可组合函数时,它会跟踪您为描述 UI 而调用的可组合函数,并将它们保存在一个组合中。重新组合是指 Compose 重新执行可能因数据更改而发生更改的可组合函数,然后更新组合以反映任何更改。
组合只能由初始组合生成,并通过重新组合更新。修改组合的唯一方法是通过重新组合。为此,Compose 需要知道要跟踪哪些状态,以便在收到更新时安排重新组合。在您的情况下,它是 amountInput
变量,因此每当它的值发生更改时,Compose 都会安排重新组合。
您在 Compose 中使用 State
和 MutableState
类型,使应用中的状态可被 Compose 观察或跟踪。State
类型是不可变的,因此您只能读取其中的值,而 MutableState
类型是可变的。您可以使用 mutableStateOf()
函数创建可观察的 MutableState
。它接收一个初始值作为参数,该参数被包装在一个 State
对象中,然后使其 value
可观察。
mutableStateOf()
函数返回的值
- 持有状态,即账单金额。
- 是可变的,因此可以更改其值。
- 是可观察的,因此 Compose 会观察值的任何更改并触发重新组合以更新 UI。
添加服务成本状态
- 在
EditNumberField()
函数中,将amountInput
状态变量之前的val
关键字更改为var
关键字
var amountInput = "0"
这使得 amountInput
可变。
- 使用
MutableState<String>
类型代替硬编码的String
变量,以便 Compose 知道跟踪amountInput
状态,然后传入"0"
字符串,这是amountInput
状态变量的初始默认值
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
var amountInput: MutableState<String> = mutableStateOf("0")
amountInput
的初始化也可以使用类型推断这样编写
var amountInput = mutableStateOf("0")
mutableStateOf()
函数接收一个初始 "0"
值作为参数,这使得 amountInput
可观察。这会导致 Android Studio 中出现此编译警告,但您很快就会修复它
Creating a state object during composition without using remember.
- 在
TextField
可组合函数中,使用amountInput.value
属性
TextField(
value = amountInput.value,
onValueChange = {},
modifier = modifier
)
Compose 会跟踪读取状态 value
属性的每个可组合函数,并在其 value
发生更改时触发重新组合。
onValueChange
回调在文本框的输入发生更改时触发。在 lambda 表达式中,it
变量包含新值。
- 在
onValueChange
命名参数的 lambda 表达式中,将amountInput.value
属性设置为it
变量
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput = mutableStateOf("0")
TextField(
value = amountInput.value,
onValueChange = { amountInput.value = it },
modifier = modifier
)
}
您正在更新 TextField
的状态(即 amountInput
变量),当 TextField
通知您文本发生更改时,通过 onValueChange
回调函数。
- 运行应用并在文本框中输入文本。文本框仍然显示
0
值,如本图所示
当用户在文本框中输入文本时,会调用 onValueChange
回调,并使用新值更新 amountInput
变量。Compose 会跟踪 amountInput
状态,因此当它的值发生更改时,会安排重新组合,并且 EditNumberField()
可组合函数会再次执行。在该可组合函数中,amountInput
变量会重置为其初始 0
值。因此,文本框显示 0
值。
使用您添加的代码,状态更改会导致安排重新组合。
但是,您需要一种方法来保留跨重新组合的 amountInput
变量的值,以便它不会在每次 EditNumberField()
函数重新组合时都重置为 0
值。您将在下一节中解决此问题。
6. 使用 remember 函数保存状态
可组合方法可以由于重新组合而被多次调用。如果可组合函数的状态未保存,则它会在重新组合期间重置其状态。
可组合函数可以使用 remember
在重新组合之间存储对象。remember
函数计算的值在初始组合期间存储在组合中,并在重新组合期间返回存储的值。通常,remember
和 mutableStateOf
函数一起在可组合函数中使用,以使状态及其更新在 UI 中正确反映。
在 EditNumberField()
函数中使用 remember
函数
- 在
EditNumberField()
函数中,使用by
remember
Kotlin 属性委托初始化amountInput
变量,方法是用remember
括起对mutableStateOf
()
函数的调用。 - 在
mutableStateOf
()
函数中,传入空字符串而不是静态的"0"
字符串
var amountInput by remember { mutableStateOf("") }
现在,空字符串是 amountInput
变量的初始默认值。by
是一个 Kotlin 属性委托。amountInput
属性的默认 getter 和 setter 函数分别委托给 remember
类的 getter 和 setter 函数。
- 导入这些函数
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
添加委托的 getter 和 setter 导入允许您读取和设置 amountInput
,而无需引用 MutableState
的 value
属性。
更新后的 EditNumberField()
函数应如下所示
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput by remember { mutableStateOf("") }
TextField(
value = amountInput,
onValueChange = { amountInput = it },
modifier = modifier
)
}
- 运行应用并在文本框中输入一些文本。您现在应该会看到您输入的文本。
7. 状态和重新组合的实际应用
在本节中,您将设置断点并调试 EditNumberField()
可组合函数,以了解初始组合和重新组合的工作原理。
设置断点并在模拟器或设备上调试应用
- 在
EditNumberField()
函数中,在名为onValueChange
的参数旁边设置行断点。 - 在导航菜单中,点击调试“app”。应用将在模拟器或设备上启动。当创建
TextField
元素时,应用的执行将首次暂停。
- 在调试窗格中,点击 恢复程序。文本框已创建。
- 在模拟器或设备上,在文本框中输入一个字母。当应用执行到达您设置的断点时,将再次暂停。
当您输入文本时,将调用 onValueChange
回调。在 lambda 中,it
包含您在键盘上输入的新值。
一旦将“it”的值分配给 amountInput
,Compose 就会使用新数据触发重新组合,因为可观察值已更改。
- 在调试窗格中,点击 恢复程序。在模拟器或设备上输入的文本显示在带有断点的行的旁边,如本图所示。
这是文本字段的状态。
- 点击 恢复程序。在模拟器或设备上显示输入的值。
8. 修改外观
在上一节中,您使文本字段正常工作。在本节中,您将增强 UI。
向文本框添加标签
每个文本框都应该有一个标签,让用户知道他们可以输入什么信息。在以下示例图像的第一部分中,标签文本位于文本字段的中间并与输入行对齐。在以下示例图像的第二部分中,当用户点击文本框以输入文本时,标签在文本框中向上移动。要了解有关文本字段解剖结构的更多信息,请参阅 解剖结构。
修改 EditNumberField()
函数以向文本字段添加标签
- 在
EditNumberField()
函数的TextField()
可组合函数中,添加一个名为label
的参数,并将其设置为一个空 lambda 表达式。
TextField(
//...
label = { }
)
- 在 lambda 表达式中,调用接受
stringResource
(R.string.
bill_amount
)
的Text()
函数。
label = { Text(stringResource(R.string.bill_amount)) },
- 在
TextField()
可组合函数中,添加名为singleLine
的参数,并将其设置为true
值。
TextField(
// ...
singleLine = true,
)
这将文本框压缩为一行水平可滚动的行,而不是多行。
- 添加名为
keyboardOptions
的参数,并将其设置为KeyboardOptions()
。
import androidx.compose.foundation.text.KeyboardOptions
TextField(
// ...
keyboardOptions = KeyboardOptions(),
)
Android 提供了一个选项来配置屏幕上显示的键盘,以输入数字、电子邮件地址、URL 和密码等。要了解有关其他键盘类型的更多信息,请参阅 KeyboardType。
- 将键盘类型设置为数字键盘以输入数字。将
KeyboardOptions
函数传递一个名为keyboardType
的参数,并将其设置为KeyboardType.Number
。
import androidx.compose.ui.text.input.KeyboardType
TextField(
// ...
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
完成的 EditNumberField()
函数应如下面的代码片段所示。
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput by remember { mutableStateOf("") }
TextField(
value = amountInput,
onValueChange = { amountInput = it },
singleLine = true,
label = { Text(stringResource(R.string.bill_amount)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = modifier
)
}
- 运行应用。
您可以在此屏幕截图中看到对键盘的更改。
9. 显示小费金额
在本节中,您将实现应用的主要功能,即计算和小费金额的功能。
在 MainActivity.kt
文件中,一个 private
calculateTip()
函数作为启动代码的一部分提供给您。您将使用此函数来计算小费金额。
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
val tip = tipPercent / 100 * amount
return NumberFormat.getCurrencyInstance().format(tip)
}
在上述方法中,您使用 NumberFormat
以货币格式显示小费。
现在您的应用可以计算小费了,但您仍然需要使用该类对其进行格式化并显示。
使用 calculateTip()
函数
用户在文本字段可组合项中输入的文本将作为 String
返回到 onValueChange
回调函数,即使用户输入的是数字也是如此。要解决此问题,您需要转换包含用户输入金额的 amountInput
值。
- 在
EditNumberField()
可组合函数中,在amountInput
定义之后创建一个名为amount
的新变量。在amountInput
变量上调用toDoubleOrNull
函数,以将String
转换为Double
。
val amount = amountInput.toDoubleOrNull()
toDoubleOrNull()
函数是一个预定义的 Kotlin 函数,它将字符串解析为 Double
数字并返回结果,如果字符串不是数字的有效表示形式,则返回 null
。
- 在语句的末尾,添加一个
?:
Elvis 运算符,当amountInput
为 null 时返回0.0
值。
val amount = amountInput.toDoubleOrNull() ?: 0.0
- 在
amount
变量之后,创建另一个名为tip
的val
变量。使用calculateTip()
初始化它,并传递amount
参数。
val tip = calculateTip(amount)
EditNumberField()
函数应如下面的代码片段所示。
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
TextField(
value = amountInput,
onValueChange = { amountInput = it },
label = { Text(stringResource(R.string.bill_amount)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
显示计算出的提示金额
您已经编写了计算小费金额的函数,下一步是显示计算出的提示金额。
- 在
TipTimeLayout()
函数中,在Column()
块的末尾,请注意显示$0.00
的文本可组合项。您将把此值更新为计算出的提示金额。
@Composable
fun TipTimeLayout() {
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// ...
Text(
text = stringResource(R.string.tip_amount, "$0.00"),
style = MaterialTheme.typography.displaySmall
)
// ...
}
}
您需要在 TipTimeLayout()
函数中访问 amountInput
变量以计算和显示提示金额,但 amountInput
变量是 EditNumberField()
可组合函数中定义的文本字段的状态,因此您还不能从 TipTimeLayout()
函数中调用它。此图像说明了代码的结构。
此结构不允许您在新 Text
可组合项中显示提示金额,因为 Text
可组合项需要访问从 amountInput
变量计算得出的 amount
变量。您需要将 amount
变量公开给 TipTimeLayout()
函数。此图像说明了所需的代码结构,该结构使 EditNumberField()
可组合项无状态。
此模式称为状态提升。在下一节中,您将提升或提升可组合项中的状态以使其无状态。
10. 状态提升
在本节中,您将学习如何决定在何处定义状态,以便能够重用和共享您的可组合函数。
在可组合函数中,您可以定义变量来保存要在 UI 中显示的状态。例如,您在 EditNumberField()
可组合函数中将 amountInput
变量定义为状态。
当您的应用变得更加复杂,并且其他可组合函数需要访问 EditNumberField()
可组合函数中的状态时,您需要考虑将状态提升(或提取)到 EditNumberField()
可组合函数之外。
了解有状态与无状态**可组合函数**
当您需要时,应该提升状态。
- 与多个可组合函数共享状态。
- 创建一个可以在您的应用中重用的无状态可组合函数。
当您从可组合函数中提取状态时,生成的该可组合函数被称为无状态。也就是说,可组合函数可以通过从其中提取状态来使其成为无状态。
无状态可组合函数是指不具有状态的可组合函数,这意味着它不保存、定义或修改新的状态。另一方面,有状态可组合函数是指拥有一个可以随时间变化的状态片段的可组合函数。
状态提升是一种将状态移动到其调用者以使组件成为无状态的模式。
当应用于可组合函数时,这通常意味着为可组合函数引入两个参数。
- 一个
value: T
参数,它是要显示的当前值。 - 一个
onValueChange: (T) -> Unit
回调 lambda,当值发生变化时会触发该回调,以便可以在其他地方更新状态,例如当用户在文本框中输入一些文本时。
提升 EditNumberField()
函数中的状态
- 更新
EditNumberField()
函数定义,通过添加value
和onValueChange
参数来提升状态。
@Composable
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
//...
value
参数的类型为 String
,onValueChange
参数的类型为 (String) -> Unit
,因此它是一个以 String
值作为输入且没有返回值的函数。onValueChange
参数用作传递给 TextField
可组合函数的 onValueChange
回调。
- 在
EditNumberField()
函数中,更新TextField()
可组合函数以使用传入的参数。
TextField(
value = value,
onValueChange = onValueChange,
// Rest of the code
)
- 提升状态,将记住的状态从
EditNumberField()
函数移动到TipTimeLayout()
函数。
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
//...
) {
//...
}
}
- 您已将状态提升到
TipTimeLayout()
,现在将其传递给EditNumberField()
。在TipTimeLayout()
函数中,更新EditNumberField
()
函数调用以使用提升的状态。
EditNumberField(
value = amountInput,
onValueChange = { amountInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
这使得 EditNumberField
成为无状态的。您已将 UI 状态提升到其祖先 TipTimeLayout()
。TipTimeLayout()
现在是状态 (amountInput
) 的所有者。
位置格式化
位置格式化用于在字符串中显示动态内容。例如,假设您希望**小费金额**文本框显示一个 xx.xx
值,该值可能是您函数中计算和格式化的任何金额。为了在 strings.xml
文件中实现此目的,您需要使用占位符参数定义字符串资源,如下面的代码片段所示。
// No need to copy.
// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>
在 Compose 代码中,您可以有多个任何类型的占位符参数。一个 string
占位符是 %s
。
请注意 TipTimeLayout()
中的文本可组合函数,您将格式化后的提示作为参数传递给 stringResource()
函数。
// No need to copy
Text(
text = stringResource(R.string.tip_amount, "$0.00"),
style = MaterialTheme.typography.displaySmall
)
- 在函数
TipTimeLayout()
中,使用tip
属性显示小费金额。更新Text
可组合函数的text
参数以使用tip
变量作为参数。
Text(
text = stringResource(R.string.tip_amount, tip),
// ...
完成后的 TipTimeLayout()
和 EditNumberField()
函数应如下面的代码片段所示。
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
)
EditNumberField(
value = amountInput,
onValueChange = { amountInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
Text(
text = stringResource(R.string.tip_amount, tip),
style = MaterialTheme.typography.displaySmall
)
Spacer(modifier = Modifier.height(150.dp))
}
}
@Composable
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = value,
onValueChange = onValueChange,
singleLine = true,
label = { Text(stringResource(R.string.bill_amount)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = modifier
)
}
总而言之,您已将 amountInput
状态从 EditNumberField()
提升到 TipTimeLayout()
可组合函数中。为了使文本框像以前一样工作,您必须向 EditNumberField()
可组合函数传递两个参数:amountInput
值和更新 amountInput
值的 lambda 回调(来自用户的输入)。这些更改使您能够根据 TipTimeLayout()
中的 amountInput
属性计算小费,并将其显示给用户。
- 在模拟器或设备上运行应用,然后在**账单金额**文本框中输入一个值。将显示账单金额 15% 的小费金额,如以下图像所示。
11. 获取解决方案代码
要下载完成的 codelab 的代码,您可以使用以下 git 命令。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git $ cd basic-android-kotlin-compose-training-tip-calculator $ git checkout state
或者,您可以将存储库下载为 zip 文件,解压缩它,然后在 Android Studio 中打开它。
如果您想查看解决方案代码,请在 GitHub 上查看。
12. 结论
恭喜!您已完成此 codelab 并学习了如何在 Compose 应用中使用状态!
摘要
- 应用中的状态是指任何可能随时间变化的值。
- 组合是 Compose 在执行可组合函数时构建的 UI 的描述。Compose 应用调用可组合函数将数据转换为 UI。
- 初始组合是指 Compose 在第一次执行可组合函数时创建 UI 的过程。
- 重新组合是指在数据发生变化时再次运行相同的可组合函数以更新树的过程。
- 状态提升是一种将状态移动到其调用者以使组件成为无状态的模式。