1. 准备工作
在此 Codelab 中,您将了解状态,以及 Jetpack Compose 如何使用和操纵状态。
从本质上讲,应用中的状态是指可以随时间变化的任何值。这个定义非常宽泛,包含应用中从数据库到变量等的所有元素。您会在后续单元中详细了解数据库,但现在,您只需要知道,数据库是结构化信息的有组织集合,就像计算机中的文件一样。
所有 Android 应用都会向用户显示状态。下面是 Android 应用中的一些状态示例:
- 在无法建立网络连接时显示的消息。
- 表单,例如注册表单。状态可以填写和提交。
- 可点按的控件,例如按钮。状态可以是“未点按”“正在点按”(显示动画)或“已点按”(一种
onClick
操作)。
在此 Codelab 中,您将探索在使用 Compose 时如何使用和看待状态。为此,您将使用以下内置 Compose 界面元素构建一个名为 Tip Time 的小费计算器应用:
TextField
可组合项,用于输入和修改文本。Text
可组合项,用于显示文本。Spacer
可组合项,用于显示界面元素之间的空白空间。
学完此 Codelab 后,您将构建一个交互式小费计算器,该计算器可在您输入服务金额后自动计算小费金额。最终应用如下图所示:
前提条件
- 对 Compose(如
@Composable
注解)有基本的了解。 - 基本熟悉 Compose 布局,例如
Row
和Column
布局可组合项。 - 基本熟悉修饰符,例如
Modifier.padding()
函数。 - 熟悉
Text
可组合项。
学习内容
- 如何看待界面中的状态。
- Compose 如何使用状态显示数据。
- 如何向应用添加文本框。
- 如何提升状态。
您将构建的内容
- 一款名为 Tip Time 的小费计算器应用,用于根据服务金额计算小费金额。
所需条件
- 可连接到互联网的计算机和网络浏览器
- 了解 Kotlin
- 最新版本的 Android Studio
2. 开始
- 请查看 Google 的在线小费计算器。请注意,这只是一个示例,它不是您将在本课程中创建的 Android 应用。
- 在 Bill 和 Tip % 框中输入不同的值。小费和总金额的值会随之改变。
请注意,在您输入值后,系统会更新 Tip 和 Total。在下面的 Codelab 结束时,您将开发类似的 Android 小费计算器应用。
在此在线课程中,您将构建一个简单的小费计算器 Android 应用。
开发者通常会采取以下开发流程:准备一个简化版的应用,并使其能够正常运行(即使它看起来不是很好),之后添加更多功能,改进其外观,使其更有吸引力。
学完此 Codelab 后,您的小费计算器应用将如以下屏幕截图所示。在用户输入 Bill Amount(账单金额)后,您的应用将显示建议的小费金额。目前,小费百分比已硬编码为 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()
函数。这样,在 Design 和 Split 窗格中便会显示该应用的布局。
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
}
}
接受用户的输入内容
在本部分中,您将添加界面元素,让用户能够在应用中输入账单金额。该界面元素如下图所示:
您的应用使用自定义样式和主题。
样式和主题是一个属性集合,用于指定单个界面元素的外观。样式可以指定某些属性,例如字体颜色、字号、背景颜色等可以应用到整个应用的属性。后续 Codelab 将介绍如何在您的应用中实现这些方面。目前,我们已为您完成了样式设置以让您的应用更美观。
为了帮助您更好地了解,下方对照比较了该应用采用和未采用自定义主题的解决方案版本。
未采用自定义主题。 | 采用了自定义主题。 |
借助 TextField
可组合函数,用户可以在应用中输入文本。例如,请注意下图中 Gmail 应用登录界面上的文本框:
将 TextField
可组合项添加到应用中:
- 在
MainActivity.kt
文件中,添加一个接受Modifier
形参的EditNumberField()
可组合函数。 - 在
TipTimeLayout()
下的EditNumberField()
函数的正文中,添加一个TextField
,用于接受设置为空字符串的value
具名形参以及一个设置为空 lambda 表达式的onValueChange
具名形参:
@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(
...
)
...
}
}
此时界面上会显示文本框。
- 在 Design 窗格中,您应该会看到
Calculate Tip
文本、一个空文本框和Tip Amount
文本可组合项。
4. 在 Compose 中使用状态
应用中的状态是指可以随时间变化的任何值。在该应用中,状态是指账单金额。
添加用于存储状态的变量:
- 在
EditNumberField()
函数的开头,使用val
关键字添加amountInput
变量,并将它设为"0"
值:
val amountInput = "0"
这是应用的账单金额状态。
- 将
value
具名形参设置为amountInput
值:
TextField(
value = amountInput,
onValueChange = {},
)
- 检查预览效果。文本框会显示设置为状态变量的值,如下图所示:
- 在模拟器中运行该应用,尝试输入其他值。硬编码状态保持不变,因为
TextField
可组合项不会自行更新。当value
形参(设置为amountInput
属性)更改时,它会更新。
amountInput
变量表示文本框的状态。具有硬编码状态并没有什么用处,因为它无法修改,也无法反映用户输入。您需要在用户更新账单金额时更新应用状态。
5. 组合
应用中的可组合项描述的界面将显示一个列,其中包含一些文本、一个空格符号和一个文本框。文本显示 Calculate Tip
文本,文本框显示 0
值或任何默认值。
Compose 是一个声明性界面框架,这意味着您可以在代码中声明界面的外观。如果您想让文本框一开始显示 100
值,请在代码中将可组合项的初始值设置为 100
值。
如果您想让界面在应用运行时或用户与应用互动时发生变化,该怎么办?例如,如果您想使用用户输入的值更新 amountInput
变量并将其显示在文本框中,该怎么办?此时,您需要依赖名为“重组”的进程来更新应用的组合。
“组合”是对 Compose 在执行可组合项时所构建界面的描述。Compose 应用调用可组合函数,以将数据转换为界面。如果发生状态更改,Compose 会使用新状态重新执行受影响的可组合函数,从而创建更新后的界面。这一过程称为“重组”。Compose 会为您安排重组。
当 Compose 首次运行可组合函数时,在初始组合期间,它会跟踪您为了描述组合中的界面而调用的可组合函数。重组是指 Compose 重新执行可能因数据更改而更改的可组合项,然后更新组合以反映所有更改。
组合只能通过初始组合生成且只能通过重组进行更新。重组是修改组合的唯一方式。为此,Compose 需要知道要跟踪的状态,以便在收到更新时能够安排重组。在本例中,这个状态就是 amountInput
变量,因此每当其值更改时,Compose 都会安排重组。
在 Compose 中,您可以使用 State
和 MutableState
类型让应用中的状态可被 Compose 观察或跟踪。State
类型不可变,因此您只能读取其中的值,而 MutableState
类型是可变的。您可以使用 mutableStateOf()
函数来创建可观察的 MutableState
。它接受初始值作为封装在 State
对象中的形参,这样便可使其 value
变为可观察。
mutableStateOf()
函数返回的值:
- 会保持状态,即账单金额。
- 可变,因此该值可以更改。
- 可观察,因此 Compose 会观察值的所有更改并触发重组以更新界面。
添加 cost-of-service 状态:
- 在
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
通过 onValueChange
回调函数通知您文本发生更改时,您将更新 TextField
的状态(即 amountInput
变量)。
- 运行应用并在文本框中输入文本。文本框仍会显示
0
值,如下图所示:
用户在文本框中输入文本后,系统会调用 onValueChange
回调,并使用新值更新 amountInput
变量。Compose 跟踪 amountInput
状态,因此当其值更改时,系统会安排重组并再次执行 EditNumberField()
可组合函数。在该可组合函数中,amountInput
变量会重置为初始 0
值。因此,文本框会显示 0
值。
在您添加代码后,状态更改会导致系统安排重组。
不过您需要利用一种方法在重组后保留 amountInput
变量的值,以免每次 EditNumberField()
函数重组时它都重置为 0
值。您将在下一部分中解决此问题。
6. 使用 remember 函数保存状态
可组合方法可能会因重组而被系统多次调用。如果不保存,可组合项就会在重组期间重置状态。
可组合函数可以使用 remember
跨重组存储对象。初始组合期间,remember
函数计算的值会存储在组合中,而存储的值会在重组期间返回。remember
和 mutableStateOf
函数通常在可组合函数中一起使用,以使状态及其更新正确反映在界面中。
在 EditNumberField()
函数中使用 remember
函数:
- 在
EditNumberField()
函数中,使用remember
将对mutableStateOf
()
的调用括起来,以便使用by
remember
Kotlin 属性委托来初始化amountInput
变量。 - 在
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()
可组合函数,以了解初始组合和重组的运作方式。
设置断点,然后在模拟器或设备上调试应用:
- 在
onValueChange
具名函数旁边的EditNumberField()
函数中,设置一个行断点。 - 在导航菜单中,点击 Debug 'app' 图标。应用会在模拟器或设备上启动。创建
TextField
元素后,应用执行会第一次暂停。
- 在 Debug 窗格中,点击 Resume Program。文本框创建完毕。
- 在模拟器或设备上的文本框中输入一个字母。当应用到达您设置的断点时,应用执行会再次暂停。
当您输入文本时,系统会调用 onValueChange
回调。在 lambda 内,it
具有您在键盘中输入的新值。
将“it”的值分配给 amountInput
后,随着可观察的值发生变化,Compose 会触发使用新数据进行重组。
- 在 Debug 窗格中,点击 Resume Program。在模拟器或设备上输入的文本会显示在包含断点的代码行旁边,如下图所示:
这是文本字段的状态。
- 点击 Resume Program。输入的值会显示在模拟器或设备上。
8. 修改外观
在上一部分中,您已经让文本字段正常运行了。在本部分中,您将改进界面。
向文本框添加标签
每个文本框都应包含一个标签,以便用户了解可以输入哪些信息。在以下示例图片的第 1 部分中,标签文本位于文本字段的中间,并与输入行对齐。在以下示例图片的第 2 部分中,当用户点击文本框以输入文本时,该标签会移到文本框中靠上的位置。如需详细了解文本字段剖析,请参阅剖析。
修改 EditNumberField()
函数,以向文本字段添加标签:
- 在
EditNumberField()
函数的TextField()
可组合函数中,添加一个设置为空 lambda 表达式的label
具名形参:
TextField(
//...
label = { }
)
- 在 lambda 表达式中,调用接受
stringResource
(R.string.
bill_amount
)
的Text()
函数:
label = { Text(stringResource(R.string.bill_amount)) },
- 在
TextField()
可组合函数中,添加设置为true
值的singleLine
具名形参:
TextField(
// ...
singleLine = true,
)
这样可以将文本框从多行压缩成可水平滚动的单行。
- 添加设置为
KeyboardOptions()
的keyboardOptions
形参:
import androidx.compose.foundation.text.KeyboardOptions
TextField(
// ...
keyboardOptions = KeyboardOptions(),
)
Android 提供了一个选项,用于配置屏幕上显示的键盘,以便输入数字、电子邮件地址、网址和密码等内容。如需详细了解其他键盘类型,请参阅 KeyboardType。
- 将键盘类型设置为数字键盘即可输入数字。向
KeyboardOptions
函数传递设置为KeyboardType.Number
的keyboardType
具名形参:
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
变量。您需要向 TipTimeLayout()
函数公开 amount
变量。所需代码结构(这会使 EditNumberField()
可组合项变为无状态)如下图所示:
这种模式称为“状态提升”。在下一部分中,您将“提升”可组合项的状态,使其变为无状态。
10. 状态提升
在本部分中,您将了解如何决定在哪里定义状态,以便能够重复使用和共享可组合项。
在可组合函数中,您可以定义一些变量,用于保存要在界面中显示的状态。例如,您在 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
变为无状态。您已将界面状态提升到其祖先实体 TipTimeLayout()
。TipTimeLayout()
现在是状态(amountInput
) 所有者。
位置格式设置
通过位置格式设置,您可以使用字符串显示动态内容。例如,假设您希望 Tip amount 文本框显示一个 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
属性计算并向用户显示小费金额。
- 在模拟器或设备上运行应用,然后在 Bill Amount 文本框中输入一个值。系统即会显示小费金额(账单金额的 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 在执行可组合项时所构建界面的描述。Compose 应用调用可组合函数,以将数据转换为界面。
- 初始组合是指 Compose 会在首次执行可组合函数时创建界面。
- 重组过程会再次运行相同的可组合项,以便在其数据发生变化时更新树。
- 状态提升是一种将状态移到其调用方以使组件变为无状态的模式。