1. 开始之前
本 Codelab 介绍了状态,以及 Jetpack Compose 如何使用和操纵状态。
从核心来讲,应用中的状态是任何随时间变化的值。这个定义非常宽泛,包括了从数据库到应用中的变量的一切。你将在后续单元中学习更多关于数据库的知识,但现在你只需要知道数据库是结构化信息的有序集合,例如计算机上的文件。
所有 Android 应用都会向用户显示状态。Android 应用中状态的一些示例包括
- 网络连接无法建立时显示的消息。
- 表单,例如注册表单。状态可以被填写和提交。
- 可点击的控件,例如按钮。状态可以是 未点击、正在点击(显示动画)或 已点击(一次
onClick
操作)。
在本 Codelab 中,您将探索如何在 Compose 中使用和思考状态。为此,您将使用这些内置的 Compose UI 元素构建一个名为 Tip Time 的小费计算器应用:
- 一个用于输入和编辑文本的
TextField
可组合项。 - 一个用于显示文本的
Text
可组合项。 - 一个用于在 UI 元素之间显示空白空间的
Spacer
可组合项。
在本 Codelab 结束时,您将构建一个交互式小费计算器,当您输入服务金额时,它会自动计算小费金额。此图片展示了最终应用的外观:
前提条件
- 对 Compose 有基本了解,例如
@Composable
注解。 - 基本熟悉 Compose 布局,例如
Row
和Column
布局可组合项。 - 基本熟悉修饰符,例如
Modifier.padding()
函数。 - 熟悉
Text
可组合项。
您将学到什么
- 如何在 UI 中思考状态。
- Compose 如何使用状态显示数据。
- 如何向应用添加文本框。
- 如何提升状态。
您将构建什么
- 一个名为 Tip Time 的小费计算器应用,它根据服务金额计算小费金额。
您需要什么
- 一台可连接互联网的计算机和网络浏览器
- Kotlin 知识
- 最新版 Android Studio
2. 开始
- 查看 Google 在线小费计算器。请注意,这只是一个示例,并非您将在本课程中创建的 Android 应用。
- 在 Bill(账单)和 Tip %(小费百分比)框中输入不同的值。小费和总金额会随之变化。
请注意,您输入值的那一刻,Tip(小费)和 Total(总金额)就会更新。在接下来的 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
此文件主要包含模板生成的代码和以下函数。
- The
TipTimeLayout()
function contains aColumn
element with two text composables that you see in the screenshots. This also hasspacer
composable to add space for aesthetic reasons. ->TipTimeLayout()
函数包含一个Column
元素,其中包含您在截图中看到的两个文本可组合项。它还包含spacer
可组合项,用于出于美观原因添加空间。 - The
calculateTip()
function that accepts the bill amount and calculates a 15% tip amount. ThetipPercent
parameter is set to a15.0
default argument value. This sets the default tip value to 15% for now. In the next codelab, you get the tip amount from the user. ->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)
}
In the onCreate()
function's Surface()
block, the TipTimeLayout()
function is being called. This displays the app's layout in the device or the emulator. -> 在 onCreate()
函数的 Surface()
块中,调用了 TipTimeLayout()
函数。这会在设备或模拟器中显示应用的布局。
override fun onCreate(savedInstanceState: Bundle?) {
//...
setContent {
TipTimeTheme {
Surface(
//...
) {
TipTimeLayout()
}
}
}
}
The TipTimeLayoutPreview()
function's TipTimeTheme
block, the TipTimeLayout()
function is being called. This displays the app's layout in the Design and in the Split pane. -> 在 TipTimeLayoutPreview()
函数的 TipTimeTheme
块中,调用了 TipTimeLayout()
函数。这会在 Design(设计)和 Split(拆分)窗格中显示应用的布局。
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
}
}
获取用户输入
在本节中,您将添加一个 UI 元素,让用户可以在应用中输入账单金额。您可以在此图片中看到它的外观:
您的应用使用自定义样式和主题。
样式和主题是指定单个 UI 元素外观的属性集合。样式可以指定字体颜色、字体大小、背景颜色等许多属性,这些属性可以应用于整个应用。后续的 Codelab 将介绍如何在您的应用中实现这些。目前,为了让您的应用更美观,这些工作已经为您完成。
为了更好地理解,以下是应用解决方案版本在有无自定义主题下的并排比较:
|
|
The TextField
composable function lets the user enter text in an app. For example, notice the text box on the login screen of the Gmail app in this image -> TextField
可组合函数允许用户在应用中输入文本。例如,请注意此图片中 Gmail 应用登录屏幕上的文本框:
将 TextField
可组合项添加到应用中
- In the
MainActivity.kt
file, add anEditNumberField()
composable function, that takes aModifier
parameter. -> 在MainActivity.kt
文件中,添加一个EditNumberField()
可组合函数,它接受一个Modifier
参数。 - In the body of the
EditNumberField()
function belowTipTimeLayout()
, add aTextField
that accepts avalue
named parameter set to an empty string and anonValueChange
named parameter set to an empty lambda expression -> 在TipTimeLayout()
下方的EditNumberField()
函数主体中,添加一个TextField
,它接受一个名为value
的参数,并将其设置为空字符串,以及一个名为onValueChange
的参数,并将其设置为空 lambda 表达式:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
- 注意您传递的参数:
- The
value
parameter is a text box that displays the string value you pass here. ->value
参数是一个文本框,用于显示您在此处传递的字符串值。 - The
onValueChange
parameter is the lambda callback that's triggered when the user enters text in the text box. ->onValueChange
参数是当用户在文本框中输入文本时触发的 lambda 回调。
- 导入此函数:
import androidx.compose.material3.TextField
- In the
TipTimeLayout()
composable, on the line after the first text composable function, call theEditNumberField()
function, passing the following modifier. -> 在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 中使用状态
应用中的状态是任何随时间变化的值。在此应用中,状态是账单金额。
添加一个变量来存储状态
- At the beginning of the
EditNumberField()
function, use theval
keyword to add anamountInput
variable set it to"0"
value -> 在EditNumberField()
函数的开头,使用val
关键字添加一个amountInput
变量,并将其设置为"0"
值:
val amountInput = "0"
这是应用的账单金额状态。
- 将名为
value
的参数设置为amountInput
值:
TextField(
value = amountInput,
onValueChange = {},
)
- 查看预览。文本框显示设置为状态变量的值,如您在此图片中看到:
- 在模拟器中运行应用,尝试输入不同的值。硬编码的状态保持不变,因为
TextField
可组合项不会自行更新。当其value
参数(设置为amountInput
属性)改变时,它才会更新。
The amountInput
variable represents the state of the text box. Having a hardcoded state isn't useful because it can't be modified and it doesn't reflect user input. You need to update the state of the app when the user updates the bill amount. -> amountInput
变量表示文本框的状态。硬编码的状态没有用,因为它无法修改,也无法反映用户输入。当用户更新账单金额时,您需要更新应用的状态。
5. 组合(Composition)
应用中的可组合项描述了一个 UI,它显示一个列,其中包含一些文本、一个间隔符和一个文本框。文本显示 Calculate Tip
文本,文本框显示 0
值或任何默认值。
Compose is a declarative UI framework, meaning that you declare how the UI should look in your code. If you wanted your text box to show a 100
value initially, you'd set the initial value in the code for the composables to a 100
value. -> Compose 是一个声明式 UI 框架,这意味着您在代码中*声明* UI 应该是什么样子。如果您希望您的文本框最初显示 100
值,您将在可组合项的代码中将初始值设置为 100
值。
What happens if you want your UI to change while the app is running or as the user interacts with the app? For example, what if you wanted to update the amountInput
variable with the value entered by the user and display it in the text box? That's when you rely on a process called recomposition to update the Composition of the app. -> 如果您想在应用运行时或用户与应用交互时更改 UI 怎么办?例如,如果您想用用户输入的值更新 amountInput
变量并在文本框中显示它,该怎么办?这时就需要依赖一个称为重组(recomposition)的过程来更新应用的组合(Composition)。
The Composition is a description of the UI built by Compose when it executes composables. Compose apps call composable functions to transform data into UI. If a state change happens, Compose re-executes the affected composable functions with the new state, which creates an updated UI—this is called recomposition. Compose schedules a recomposition for you. -> *组合(Composition)*是 Compose 执行可组合项时构建的 UI 描述。Compose 应用调用可组合函数将数据转换为 UI。如果发生状态变化,Compose 会使用新状态重新执行受影响的可组合函数,从而创建更新的 UI——这称为*重组(recomposition)*。Compose 会为您安排*重组*。
When Compose runs your composables for the first time during initial composition, it keeps track of the composables that you call to describe your UI in a Composition. Recomposition is when Compose re-executes the composables that may have changed in response to data changes and then updates the Composition to reflect any changes. -> 在*初始组合*期间,当 Compose 首次运行您的可组合项时,它会在组合(Composition)中跟踪您调用以描述 UI 的可组合项。*重组(Recomposition)*是指 Compose 响应数据变化重新执行可能已更改的可组合项,然后更新组合(Composition)以反映任何更改。
The Composition can only be produced by an initial composition and updated by recomposition. The only way to modify the Composition is through recomposition. To do this, Compose needs to know what state to track so that it can schedule the recomposition when it receives an update. In your case, it's the amountInput
variable, so whenever its value changes, Compose schedules a recomposition. -> 组合(Composition)只能通过*初始组合*生成,并由*重组*更新。修改组合的唯一方法是通过*重组*。为此,*Compose 需要知道要跟踪哪些状态*,以便在收到更新时安排重组。在您的案例中,它是 amountInput
变量,因此无论其值何时更改,Compose 都会安排一次重组。
You use the State
and MutableState
types in Compose to make state in your app observable, or tracked, by Compose. The State
type is immutable, so you can only read the value in it, while the MutableState
type is mutable. You can use the mutableStateOf()
function to create an observable MutableState
. It receives an initial value as a parameter that is wrapped in a State
object, which then makes its value
observable. -> 您在 Compose 中使用 State
和 MutableState
类型,使应用中的状态可由 Compose 观察或跟踪。State
类型是不可变的,因此您只能读取其中的值,而 MutableState
类型是可变的。您可以使用 mutableStateOf()
函数创建一个可观察的 MutableState
。它接收一个初始值作为参数,该值被包装在 State
对象中,从而使其 value
可观察。
The value returned by the mutableStateOf()
function -> mutableStateOf()
函数返回的值:
- 持有状态,即账单金额。
- 是可变的,因此值可以更改。
- 是可观察的,因此 Compose 会观察对值的任何更改并触发重组以更新 UI。
添加服务成本状态
- In the
EditNumberField()
function, change theval
keyword before theamountInput
state variable to thevar
keyword -> 在EditNumberField()
函数中,将amountInput
状态变量之前的val
关键字更改为var
关键字:
var amountInput = "0"
这使得 amountInput
可变。
- Use the
MutableState<String>
type instead of the hardcodedString
variable so that Compose knows to track theamountInput
state and then pass in a"0"
string, which is the initial default value for theamountInput
state variable -> 使用MutableState<String>
类型代替硬编码的String
变量,以便 Compose 知道跟踪amountInput
状态,然后传入"0"
字符串,这是amountInput
状态变量的初始默认值:
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
var amountInput: MutableState<String> = mutableStateOf("0")
The amountInput
initialization can also be written like this with type inference -> amountInput
初始化也可以使用类型推断这样编写:
var amountInput = mutableStateOf("0")
The mutableStateOf()
function receives an initial "0"
value as an argument, which then makes amountInput
observable. This results in this compilation warning in Android Studio, but you fix it soon -> mutableStateOf()
函数接收一个初始的 "0"
值作为参数,这使得 amountInput
可观察。这会在 Android Studio 中导致此编译警告,但您很快就会修复它:
Creating a state object during composition without using remember.
- In the
TextField
composable function, use theamountInput.value
property -> 在TextField
可组合函数中,使用amountInput.value
属性:
TextField(
value = amountInput.value,
onValueChange = {},
modifier = modifier
)
Compose keeps track of each composable that reads state value
properties and triggers a recomposition when its value
changes. -> Compose 会跟踪读取状态 value
属性的每个可组合项,并在其 value
更改时触发重组。
The onValueChange
callback is triggered when the text box's input changes. In the lambda expression, the it
variable contains the new value. -> 当文本框的输入发生变化时,会触发 onValueChange
回调。在 lambda 表达式中,it
变量包含新值。
- In the
onValueChange
named parameter's lambda expression, set theamountInput.value
property to theit
variable -> 在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
)
}
You are updating the state of the TextField
(that is the amountInput
variable), when the TextField
notifies you that there is a change in the text through onValueChange
callback function. -> 当 TextField
通过 onValueChange
回调函数通知您文本发生更改时,您正在更新 TextField
的状态(即 amountInput
变量)。
- Run the app and enter text in the text box. The text box still shows a
0
value as you can see in this image -> 运行应用并在文本框中输入文本。文本框仍然显示0
值,如您在此图片中看到:
When the user enters text in the text box, the onValueChange
callback is called and the amountInput
variable is updated with the new value. The amountInput
state is tracked by Compose, so the moment that its value changes, recomposition is scheduled and the EditNumberField()
composable function is executed again. In that composable function, the amountInput
variable is reset to its initial 0
value. Thus, the text box shows a 0
value. -> 当用户在文本框中输入文本时,会调用 onValueChange
回调,并使用新值更新 amountInput
变量。amountInput
状态由 Compose 跟踪,因此其值发生变化的瞬间,就会安排重组,并再次执行 EditNumberField()
可组合函数。在该可组合函数中,amountInput
变量会重置为其初始的 0
值。因此,文本框显示 0
值。
使用您添加的代码,状态更改会导致安排重组。
However, you need a way to preserve the value of the amountInput
variable across recompositions so that it's not reset to a 0
value each time that the EditNumberField()
function recomposes. You resolve this issue in the next section. -> 然而,您需要一种方法来跨重组保留 amountInput
变量的值,以便在 EditNumberField()
函数每次重组时,它不会重置为 0
值。您将在下一节中解决这个问题。
6. 使用 remember 函数保存状态
Composable methods can be called many times because of recomposition. The composable resets its state during recomposition if it's not saved. -> 可组合方法由于重组可能被多次调用。如果未保存,可组合项会在重组期间重置其状态。
Composable functions can store an object across recompositions with the remember
. A value computed by the remember
function is stored in the Composition during initial composition and the stored value is returned during recomposition. Usually remember
and mutableStateOf
functions are used together in composable functions to have the state and its updates be reflected properly in the UI. -> 可组合函数可以使用 remember
在重组期间存储对象。remember
函数计算的值在初始组合期间存储在组合(Composition)中,并在重组期间返回存储的值。remember
和 mutableStateOf
函数通常在可组合函数中一起使用,以使状态及其更新正确反映在 UI 中。
在 EditNumberField()
函数中使用 remember
函数
- In the
EditNumberField()
function, initialize theamountInput
variable with theby
remember
Kotlin property delegate, by surrounding the call tomutableStateOf
()
function withremember
. -> 在EditNumberField()
函数中,使用by
remember
Kotlin 属性委托初始化amountInput
变量,通过用remember
围绕对mutableStateOf
()
函数的调用。 - In the
mutableStateOf
()
function, pass in an empty string instead of a static"0"
string -> 在mutableStateOf
()
函数中,传入一个空字符串而不是静态的"0"
字符串:
var amountInput by remember { mutableStateOf("") }
Now the empty string is the initial default value for the amountInput
variable. by
is a Kotlin property delegation. The default getter and setter functions for the amountInput
property are delegated to the remember
class's getter and setter functions, respectively. -> 现在空字符串是 amountInput
变量的初始默认值。by
是一个 Kotlin 属性委托。amountInput
属性的默认 getter 和 setter 函数分别委托给 remember
类的 getter 和 setter 函数。
- 导入这些函数:
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Adding the delegate's getter and setter imports lets you read and set amountInput
without referring to the MutableState
's value
property. -> 添加委托的 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
命名参数的那一行,设置一个行断点。 - In the navigation menu, click Debug ‘app'. The app launches on the emulator or device. Your app's execution pauses for the first time when the
TextField
element is created. -> 在导航菜单中,点击 Debug ‘app'(调试“应用”)。应用将在模拟器或设备上启动。当创建TextField
元素时,您的应用执行将首次暂停。
- In the Debug pane, click
Resume Program. The text box is created. -> 在 Debug(调试)窗格中,点击
Resume Program(恢复程序)。文本框已创建。
- On the emulator or device, enter a letter in the text box. Your app's execution pauses again when it reaches the breakpoint that you set. -> 在模拟器或设备上,在文本框中输入一个字母。当应用执行到达您设置的断点时,会再次暂停。
When you enter the text, the onValueChange
callback is called. Inside the lambda it
has the new value you typed in the keypad. -> 当您输入文本时,会调用 onValueChange
回调。在 lambda 内部,it
包含您在键盘中输入的新值。
Once the value of "it" is assigned to amountInput
then Compose triggers the recomposition with the new data as the observable value has changed. -> 一旦将“it”的值分配给 amountInput
,Compose 就会使用新数据触发重组,因为可观察值已更改。
- In the Debug pane, click
Resume Program. The text entered in the emulator or on the device displays next to the line with the breakpoint as seen in this image -> 在 Debug(调试)窗格中,点击
Resume Program(恢复程序)。在模拟器或设备中输入的文本将显示在断点所在行旁边,如在此图片中看到:
这是文本字段的状态。
- Click
Resume Program. The value entered is displayed on the emulator or device. -> 点击
Resume Program(恢复程序)。输入的值将显示在模拟器或设备上。
8. 修改外观
在上一节中,您成功地让文本字段工作了。在本节中,您将改进 UI。
为文本框添加标签
Every text box should have a label that lets users know what information they can enter. In the first part of the following example image, the label text is in the middle of a text field and aligned with the input line. In the second part of the following example image, the label is moved higher in the text box when the user clicks in the text box to enter text. To learn more about text-field anatomy, see Anatomy. -> 每个文本框都应该有一个标签,让用户知道他们可以输入什么信息。在下面示例图片的第一部分中,标签文本位于文本字段中间,与输入行对齐。在示例图片的第二部分中,当用户点击文本框输入文本时,标签会向上移动到文本框的上方。要了解更多关于文本字段结构的信息,请参阅 Anatomy(结构)。
修改 EditNumberField()
函数,为文本字段添加标签
- In the
EditNumberField()
function'sTextField()
composable function , add alabel
named parameter set to an empty lambda expression -> 在EditNumberField()
函数的TextField()
可组合函数中,添加一个名为label
的参数,并将其设置为空 lambda 表达式:
TextField(
//...
label = { }
)
- In the lambda expression, call the
Text()
function that accepts astringResource
(R.string.
bill_amount
)
-> 在 lambda 表达式中,调用接受 *stringResource
*(R.string.
*bill_amount
*)
的Text()
函数:
label = { Text(stringResource(R.string.bill_amount)) },
- In the
TextField()
composable function, addsingleLine
named parameter set to atrue
value -> 在TextField()
可组合函数中,添加一个名为singleLine
的参数,并将其设置为true
值:
TextField(
// ...
singleLine = true,
)
这将把文本框从多行压缩为单行可水平滚动的行。
- 添加
keyboardOptions
参数,并将其设置为KeyboardOptions()
:
import androidx.compose.foundation.text.KeyboardOptions
TextField(
// ...
keyboardOptions = KeyboardOptions(),
)
Android provides an option to configure the keyboard displayed on the screen to enter digits, email addresses, URLs, and passwords, to name a few. To learn more about other keyboard types, see KeyboardType. -> Android 提供了一个选项来配置屏幕上显示的键盘,以输入数字、电子邮件地址、URL 和密码等。要了解更多关于其他键盘类型的信息,请参阅 KeyboardType。
- Set the keyboard type to number keyboard to input digits. Pass the
KeyboardOptions
function akeyboardType
named parameter set to aKeyboardType.Number
-> 将键盘类型设置为数字键盘以输入数字。向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. 显示小费金额
在本节中,您将实现应用的主要功能,即计算和显示小费金额的能力。
In the MainActivity.kt
file, a private
calculateTip()
function is given to you as part of the starter code. You will use this function to calculate the tip amount -> 在 MainActivity.kt
文件中,起始代码已提供了一个 private
calculateTip()
函数。您将使用此函数计算小费金额:
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
val tip = tipPercent / 100 * amount
return NumberFormat.getCurrencyInstance().format(tip)
}
In the above method you are using NumberFormat
to display the format of the tip as currency. -> 在上述方法中,您使用了 NumberFormat
来以货币格式显示小费。
现在您的应用可以计算小费了,但您仍然需要使用该类来格式化和显示它。
使用 calculateTip()
函数
The text entered by the user in the text field composable is returned to the onValueChange
callback function as a String
even though the user entered a number. To fix this, you need to convert the amountInput
value, which contains the amount entered by the user. -> 用户在文本字段可组合项中输入的文本作为 String
返回给 onValueChange
回调函数,即使用户输入的是数字。要解决此问题,您需要转换包含用户输入的金额的 amountInput
值。
- In
EditNumberField()
composable function, create a new variable calledamount
after theamountInput
definition. Call thetoDoubleOrNull
function on theamountInput
variable, to convert theString
to aDouble
-> 在EditNumberField()
可组合函数中,在定义amountInput
之后创建一个名为amount
的新变量。对amountInput
变量调用toDoubleOrNull
函数,将String
转换为Double
:
val amount = amountInput.toDoubleOrNull()
The toDoubleOrNull()
function is a predefined Kotlin function that parses a string as a Double
number and returns the result or null
if the string isn't a valid representation of a number. -> toDoubleOrNull()
函数是一个预定义的 Kotlin 函数,它将字符串解析为 Double
数字并返回结果,如果字符串不是有效的数字表示,则返回 null
。
- At the end of the statement, add an
?:
Elvis operator that returns a0.0
value whenamountInput
is null -> 在语句末尾,添加一个?:
Elvis 运算符,当amountInput
为 null 时,它返回0.0
值:
val amount = amountInput.toDoubleOrNull() ?: 0.0
- After the
amount
variable, create anotherval
variable calledtip
. Initialize it with thecalculateTip()
, passing theamount
parameter. -> 在amount
变量之后,创建另一个名为tip
的val
变量。使用calculateTip()
初始化它,并传入amount
参数。
val tip = calculateTip(amount)
The EditNumberField()
function should look like this code snippet -> 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)
)
}
显示计算后的小费金额
You have written the function to calculate the tip amount, the next step is to display the calculated tip amount -> 您已经编写了计算小费金额的函数,下一步是显示计算后的小费金额:
- In the
TipTimeLayout()
function at the end of theColumn()
block, notice the text composable that displays$0.00
. You will update this value to the calculated tip amount. -> 在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
)
// ...
}
}
You need to access the amountInput
variable in the TipTimeLayout()
function to calculate and display the tip amount, but the amountInput
variable is the state of the text field defined in the EditNumberField()
composable function, so you can't call it from the TipTimeLayout()
function yet. This image illustrates the structure of the code -> 您需要在 TipTimeLayout()
函数中访问 amountInput
变量来计算并显示小费金额,但 amountInput
变量是在 EditNumberField()
可组合函数中定义的文本字段的状态,因此您还不能从 TipTimeLayout()
函数中调用它。此图片展示了代码结构:
This structure won't let you display the tip amount in the new Text
composable because the Text
composable needs to access the amount
variable calculated from the amountInput
variable. You need to expose the amount
variable to the TipTimeLayout()
function. This image illustrates the desired code structure, which makes the EditNumberField()
composable stateless -> 此结构不允许您在新的 Text
可组合项中显示小费金额,因为 Text
可组合项需要访问从 amountInput
变量计算出的 amount
变量。您需要将 amount
变量暴露给 TipTimeLayout()
函数。此图片展示了期望的代码结构,这使得 EditNumberField()
可组合项无状态:
这种模式称为*状态提升*(state hoisting)。在下一节中,您将*提升*(hoist),或称之为*提起*(lift),从一个可组合项中提取状态,使其成为无状态可组合项。
10. 状态提升
在本节中,您将学习如何决定在哪里定义状态,以便可以重用和共享可组合项。
In a composable function, you can define variables that hold state to display in the UI. For example, you defined the amountInput
variable as state in the EditNumberField()
composable. -> 在可组合函数中,您可以定义持有状态的变量以在 UI 中显示。例如,您在 EditNumberField()
可组合项中将 amountInput
变量定义为状态。
When your app becomes more complex and other composables need access to the state within the EditNumberField()
composable, you need to consider hoisting, or extracting, the state out of the EditNumberField()
composable function. -> 当您的应用变得更加复杂,并且其他可组合项需要访问 EditNumberField()
可组合项中的状态时,您需要考虑将状态从 EditNumberField()
可组合函数中提升或提取出来。
理解有状态(stateful)与无状态(stateless)可组合项
您应该在需要时提升状态:
- 与多个可组合函数共享状态。
- 创建一个可在应用中重用的无状态可组合项。
When you extract state from a composable function, the resulting composable function is called stateless. That is, composable functions can be made stateless by extracting state from them. -> 当您从可组合函数中提取状态时,由此产生的可组合函数称为无状态。也就是说,可以通过从可组合函数中提取状态来使其成为无状态。
A stateless composable is a composable that doesn't have a state, meaning it doesn't hold, define, or modify a new state. On the other hand, a stateful composable is a composable that owns a piece of state that can change over time. -> *无状态*可组合项是指没有状态的可组合项,这意味着它不持有、定义或修改新状态。另一方面,*有状态*可组合项是指拥有随时间变化的状态片段的可组合项。
状态提升是将状态移动到其调用者以使组件无状态的一种模式。
当应用于可组合项时,这通常意味着向可组合项引入两个参数:
- 一个
value: T
参数,它是要显示当前值。 - 一个
onValueChange: (T) -> Unit
– 回调 lambda,当值改变时触发,以便可以在其他地方更新状态,例如当用户在文本框中输入一些文本时。
在 EditNumberField()
函数中提升状态
- Update the
EditNumberField()
function definition, to hoist the state by adding thevalue
andonValueChange
parameters -> 更新EditNumberField()
函数定义,通过添加value
和onValueChange
参数来提升状态:
@Composable
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
//...
The value
parameter is of String
type, and the onValueChange
parameter is of (String) -> Unit
type, so it's a function that takes a String
value as input and has no return value. The onValueChange
parameter is used as the onValueChange
callback passed into the TextField
composable. -> 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
)的拥有者。
位置格式化
Positional formatting is used to display dynamic content in strings. For example, imagine that you want the Tip amount text box to display an xx.xx
value that could be any amount calculated and formatted in your function. To accomplish this in the strings.xml
file, you need to define the string resource with a placeholder argument, like this code snippet -> 位置格式化用于在字符串中显示动态内容。例如,想象一下您希望 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>
In the compose code, you can have multiple, and any type of, placeholder arguments. A string
placeholder is %s
. -> 在 compose 代码中,您可以有多个任意类型的占位符参数。一个 string
占位符是 %s
。
Notice the text composable in TipTimeLayout()
, you pass in the formatted tip as an argument to the stringResource()
function. -> 请注意 TipTimeLayout()
中的文本可组合项,您将格式化后的小费作为参数传递给 stringResource()
函数。
// No need to copy
Text(
text = stringResource(R.string.tip_amount, "$0.00"),
style = MaterialTheme.typography.displaySmall
)
- In the function,
TipTimeLayout()
, use thetip
property to display the tip amount. Update theText
composable'stext
parameter to use thetip
variable as a parameter. -> 在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 应用中使用状态!
总结
- 应用中的状态是任何随时间变化的值。
- 组合(Composition)是 Compose 执行可组合项时构建的 UI 描述。Compose 应用调用可组合函数将数据转换为 UI。
- 初始组合是 Compose 首次执行可组合函数时创建 UI 的过程。
- 重组是在数据变化时再次运行相同的可组合项以更新树的过程。
- 状态提升是将状态移动到其调用者以使组件无状态的一种模式。