简介:Compose 中的状态

1. 准备工作

在此 Codelab 中,您将了解状态,以及 Jetpack Compose 如何使用和操纵状态。

从本质上讲,应用中的状态是指可以随时间变化的任何值。这个定义非常宽泛,包含应用中从数据库到变量等的所有元素。您会在后续单元中详细了解数据库,但现在,您只需要知道,数据库是结构化信息的有组织集合,就像计算机中的文件一样。

所有 Android 应用都会向用户显示状态。下面是 Android 应用中的一些状态示例:

  • 在无法建立网络连接时显示的消息。
  • 表单,例如注册表单。状态可以填写和提交。
  • 可点按的控件,例如按钮。状态可以是“未点按”“正在点按”(显示动画)或“已点按”(一种 onClick 操作)。

在此 Codelab 中,您将探索在使用 Compose 时如何使用和看待状态。为此,您将使用以下内置 Compose 界面元素构建一个名为 Tip Time 的小费计算器应用:

  • TextField 可组合项,用于输入和修改文本。
  • Text 可组合项,用于显示文本。
  • Spacer 可组合项,用于显示界面元素之间的空白空间。

学完此 Codelab 后,您将构建一个交互式小费计算器,该计算器可在您输入服务金额后自动计算小费金额。最终应用如下图所示:

e82cbb534872abcf.png

前提条件

  • 对 Compose(如 @Composable 注解)有基本的了解。
  • 基本熟悉 Compose 布局,例如 RowColumn 布局可组合项。
  • 基本熟悉修饰符,例如 Modifier.padding() 函数。
  • 熟悉 Text 可组合项。

学习内容

  • 如何看待界面中的状态。
  • Compose 如何使用状态显示数据。
  • 如何向应用添加文本框。
  • 如何提升状态。

您将构建的内容

  • 一款名为 Tip Time 的小费计算器应用,用于根据服务金额计算小费金额。

所需条件

  • 可连接到互联网的计算机和网络浏览器
  • 了解 Kotlin
  • 最新版本的 Android Studio

2. 开始

  1. 请查看 Google 的在线小费计算器。请注意,这只是一个示例,它不是您将在本课程中创建的 Android 应用。

46bf4366edc1055f.png 18da3c120daa0759.png

  1. BillTip % 框中输入不同的值。小费和总金额的值会随之改变。

c0980ba3e9ebba02.png

请注意,在您输入值后,系统会更新 TipTotal。在下面的 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 代码库中浏览该起始代码。

起始应用概览

若要熟悉起始代码,请完成以下步骤:

  1. 在 Android Studio 中打开包含起始代码的项目。
  2. 在 Android 设备或模拟器上运行该应用。
  3. 您会发现两个文本组件;一个用于显示标签,另一个用于显示小费金额。

e85b767a43c69a97.png

起始代码演示

起始代码包含文本可组合项。在本在线课程中,您将添加一个文本字段以供用户输入。下面简要介绍了一些文件,以帮助您上手。

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() 函数。这样,在 DesignSplit 窗格中便会显示该应用的布局。

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
   TipTimeTheme {
       TipTimeLayout()
   }
}

ae11354e61d2a2b9.png

接受用户的输入内容

在本部分中,您将添加界面元素,让用户能够在应用中输入账单金额。该界面元素如下图所示:

58671affa01fb9e1.png

您的应用使用自定义样式和主题。

样式和主题是一个属性集合,用于指定单个界面元素的外观。样式可以指定某些属性,例如字体颜色、字号、背景颜色等可以应用到整个应用的属性。后续 Codelab 将介绍如何在您的应用中实现这些方面。目前,我们已为您完成了样式设置以让您的应用更美观。

为了帮助您更好地了解,下方对照比较了该应用采用和未采用自定义主题的解决方案版本。

未采用自定义主题。

采用了自定义主题。

借助 TextField 可组合函数,用户可以在应用中输入文本。例如,请注意下图中 Gmail 应用登录界面上的文本框:

一个显示 Gmail 应用的手机屏幕,其中有用于输入电子邮件地址的文本字段

TextField 可组合项添加到应用中:

  1. MainActivity.kt 文件中,添加一个接受 Modifier 形参的 EditNumberField() 可组合函数。
  2. TipTimeLayout() 下的 EditNumberField() 函数的正文中,添加一个 TextField,用于接受设置为空字符串的 value 具名形参以及一个设置为空 lambda 表达式的 onValueChange 具名形参:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   TextField(
      value = "",
      onValueChange = {},
      modifier = modifier
   )
}
  1. 请注意您传递的形参:
  • value 形参是一个文本框,用于显示您在此处传递的字符串值。
  • onValueChange 形参是用户在文本框中输入文本时触发的 lambda 回调。
  1. 导入以下函数:
import androidx.compose.material3.TextField
  1. 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(
           ...
       )
       ...
   }
}

此时界面上会显示文本框。

  1. Design 窗格中,您应该会看到 Calculate Tip 文本、一个空文本框和 Tip Amount 文本可组合项。

2c208378cd4b8d41.png

4. 在 Compose 中使用状态

应用中的状态是指可以随时间变化的任何值。在该应用中,状态是指账单金额。

添加用于存储状态的变量:

  1. EditNumberField() 函数的开头,使用 val 关键字添加 amountInput 变量,并将它设为 "0" 值:
val amountInput = "0"

这是应用的账单金额状态。

  1. value 具名形参设置为 amountInput 值:
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. 检查预览效果。文本框会显示设置为状态变量的值,如下图所示:

e8e24821adfd9d8c.png

  1. 在模拟器中运行该应用,尝试输入其他值。硬编码状态保持不变,因为 TextField 可组合项不会自行更新。当 value 形参(设置为 amountInput 属性)更改时,它会更新。

amountInput 变量表示文本框的状态。具有硬编码状态并没有什么用处,因为它无法修改,也无法反映用户输入。您需要在用户更新账单金额时更新应用状态。

5. 组合

应用中的可组合项描述的界面将显示一个列,其中包含一些文本、一个空格符号和一个文本框。文本显示 Calculate Tip 文本,文本框显示 0 值或任何默认值。

Compose 是一个声明性界面框架,这意味着您可以在代码中声明界面的外观。如果您想让文本框一开始显示 100 值,请在代码中将可组合项的初始值设置为 100 值。

如果您想让界面在应用运行时或用户与应用互动时发生变化,该怎么办?例如,如果您想使用用户输入的值更新 amountInput 变量并将其显示在文本框中,该怎么办?此时,您需要依赖名为“重组”的进程来更新应用的组合。

“组合”是对 Compose 在执行可组合项时所构建界面的描述。Compose 应用调用可组合函数,以将数据转换为界面。如果发生状态更改,Compose 会使用新状态重新执行受影响的可组合函数,从而创建更新后的界面。这一过程称为“重组”。Compose 会为您安排重组。

当 Compose 首次运行可组合函数时,在初始组合期间,它会跟踪您为了描述组合中的界面而调用的可组合函数。重组是指 Compose 重新执行可能因数据更改而更改的可组合项,然后更新组合以反映所有更改。

组合只能通过初始组合生成且只能通过重组进行更新。重组是修改组合的唯一方式。为此,Compose 需要知道要跟踪的状态,以便在收到更新时能够安排重组。在本例中,这个状态就是 amountInput 变量,因此每当其值更改时,Compose 都会安排重组。

在 Compose 中,您可以使用 StateMutableState 类型让应用中的状态可被 Compose 观察或跟踪。State 类型不可变,因此您只能读取其中的值,而 MutableState 类型是可变的。您可以使用 mutableStateOf() 函数来创建可观察的 MutableState。它接受初始值作为封装在 State 对象中的形参,这样便可使其 value 变为可观察。

mutableStateOf() 函数返回的值:

  • 会保持状态,即账单金额。
  • 可变,因此该值可以更改。
  • 可观察,因此 Compose 会观察值的所有更改并触发重组以更新界面。

添加 cost-of-service 状态:

  1. EditNumberField() 函数中,将 amountInput 状态变量前面的 val 关键字更改为 var 关键字:
var amountInput = "0"

这会使 amountInput 可变。

  1. 使用 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.
  1. TextField 可组合函数中,使用 amountInput.value 属性:
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

Compose 会跟踪每个读取状态 value 属性的可组合项,并在其 value 更改时触发重组。

当文本框的输入更改时,系统会触发 onValueChange 回调。在 lambda 表达式中,it 变量包含新值。

  1. 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 变量)。

  1. 运行应用并在文本框中输入文本。文本框仍会显示 0 值,如下图所示:

3a2c62f8ec55e339.gif

用户在文本框中输入文本后,系统会调用 onValueChange 回调,并使用新值更新 amountInput 变量。Compose 跟踪 amountInput 状态,因此当其值更改时,系统会安排重组并再次执行 EditNumberField() 可组合函数。在该可组合函数中,amountInput 变量会重置为初始 0 值。因此,文本框会显示 0 值。

在您添加代码后,状态更改会导致系统安排重组。

不过您需要利用一种方法在重组后保留 amountInput 变量的值,以免每次 EditNumberField() 函数重组时它都重置为 0 值。您将在下一部分中解决此问题。

6. 使用 remember 函数保存状态

可组合方法可能会因重组而被系统多次调用。如果不保存,可组合项就会在重组期间重置状态。

可组合函数可以使用 remember 跨重组存储对象。初始组合期间,remember 函数计算的值会存储在组合中,而存储的值会在重组期间返回。remembermutableStateOf 函数通常在可组合函数中一起使用,以使状态及其更新正确反映在界面中。

EditNumberField() 函数中使用 remember 函数:

  1. EditNumberField() 函数中,使用 remember 将对 mutableStateOf() 的调用括起来,以便使用 by remember Kotlin 属性委托来初始化 amountInput 变量。
  2. mutableStateOf() 函数中,传入一个空字符串(而非静态 "0" 字符串):
var amountInput by remember { mutableStateOf("") }

现在,空字符串是 amountInput 变量的初始默认值。byKotlin 属性委托amountInput 属性的默认 getter 和 setter 函数分别委托给 remember 类的 getter 和 setter 函数。

  1. 导入以下函数:
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

通过添加委托的 getter 和 setter 导入内容,您可以读取和设置 amountInput,而无需引用 MutableStatevalue 属性。

更新后的 EditNumberField() 函数应如下所示:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       modifier = modifier
   )
}
  1. 运行应用并在文本框中输入一些文本。现在,您应该会看到您输入的文本。

59ac301a208b47c4.png

7. 状态和重组的实际运用

在本部分中,您将设置一个断点并调试 EditNumberField() 可组合函数,以了解初始组合和重组的运作方式。

设置断点,然后在模拟器或设备上调试应用:

  1. onValueChange 具名函数旁边的 EditNumberField() 函数中,设置一个行断点。
  2. 在导航菜单中,点击 Debug 'app' 图标。应用会在模拟器或设备上启动。创建 TextField 元素后,应用执行会第一次暂停。

154e060231439307.png

  1. Debug 窗格中,点击 2a29a3bad712bec.png Resume Program。文本框创建完毕。
  2. 在模拟器或设备上的文本框中输入一个字母。当应用到达您设置的断点时,应用执行会再次暂停。

当您输入文本时,系统会调用 onValueChange 回调。在 lambda 内,it 具有您在键盘中输入的新值。

将“it”的值分配给 amountInput 后,随着可观察的值发生变化,Compose 会触发使用新数据进行重组。

1d5e08d32052d02e.png

  1. Debug 窗格中,点击 2a29a3bad712bec.png Resume Program。在模拟器或设备上输入的文本会显示在包含断点的代码行旁边,如下图所示:

1f5db6ab5ca5b477.png

这是文本字段的状态。

  1. 点击 2a29a3bad712bec.png Resume Program。输入的值会显示在模拟器或设备上。

8. 修改外观

在上一部分中,您已经让文本字段正常运行了。在本部分中,您将改进界面。

向文本框添加标签

每个文本框都应包含一个标签,以便用户了解可以输入哪些信息。在以下示例图片的第 1 部分中,标签文本位于文本字段的中间,并与输入行对齐。在以下示例图片的第 2 部分中,当用户点击文本框以输入文本时,该标签会移到文本框中靠上的位置。如需详细了解文本字段剖析,请参阅剖析

a2afd6c7fc547b06.png

修改 EditNumberField() 函数,以向文本字段添加标签:

  1. EditNumberField() 函数的 TextField() 可组合函数中,添加一个设置为空 lambda 表达式的 label 具名形参:
TextField(
//...
   label = { }
)
  1. 在 lambda 表达式中,调用接受 stringResource(R.string.bill_amount)Text() 函数:
label = { Text(stringResource(R.string.bill_amount)) },
  1. TextField() 可组合函数中,添加设置为 true 值的 singleLine 具名形参:
TextField(
  // ...
   singleLine = true,
)

这样可以将文本框从多行压缩成可水平滚动的单行。

  1. 添加设置为 KeyboardOptions()keyboardOptions 形参:
import androidx.compose.foundation.text.KeyboardOptions

TextField(
  // ...
   keyboardOptions = KeyboardOptions(),
)

Android 提供了一个选项,用于配置屏幕上显示的键盘,以便输入数字、电子邮件地址、网址和密码等内容。如需详细了解其他键盘类型,请参阅 KeyboardType

  1. 将键盘类型设置为数字键盘即可输入数字。向 KeyboardOptions 函数传递设置为 KeyboardType.NumberkeyboardType 具名形参:
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
    )
}
  1. 运行应用。

您可在以下屏幕截图中看到对拨号键盘所做的更改:

55936268bf007ee9.png

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 值,其中包含用户输入的金额。

  1. EditNumberField() 可组合函数中,于 amountInput 定义之后创建一个名为 amount 的新变量。对 amountInput 变量调用 toDoubleOrNull 函数,以将 String 转换为 Double
val amount = amountInput.toDoubleOrNull()

toDoubleOrNull() 函数是一个预定义的 Kotlin 函数,该函数会将字符串解析为 Double 数字并返回结果,而如果字符串不是有效的数字表示法,该函数会返回 null

  1. 在语句末尾,添加一个 ?: Elvis 运算符,以在 amountInput 为 null 时返回 0.0 值:
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. amount 变量后面,再创建一个名为 tipval 变量。使用 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)
   )
}

显示计算出的小费金额

您已经编写了用于计算小费金额的函数,下一步是显示计算出的小费金额:

  1. 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() 函数对其进行调用。相应代码的结构如下图所示:

50bf0b9d18ede6be.png

该结构不允许您在新的 Text 可组合项中显示小费金额,因为 Text 可组合项需要访问根据 amountInput 变量计算得出的 amount 变量。您需要向 TipTimeLayout() 函数公开 amount 变量。所需代码结构(这会使 EditNumberField() 可组合项变为无状态)如下图所示:

ab4ec72388149f7c.png

这种模式称为“状态提升”。在下一部分中,您将“提升”可组合项的状态,使其变为无状态。

10. 状态提升

在本部分中,您将了解如何决定在哪里定义状态,以便能够重复使用和共享可组合项。

在可组合函数中,您可以定义一些变量,用于保存要在界面中显示的状态。例如,您在 EditNumberField() 可组合项中将 amountInput 变量定义为状态。

当您的应用变得越来越复杂并且其他可组合项需要访问 EditNumberField() 可组合项中的状态时,您需要考虑将 EditNumberField() 可组合函数中的状态提升或提取出来。

了解有状态和无状态可组合项

当您需要执行以下操作时,应该提升状态:

  • 与多个可组合函数共享状态。
  • 创建可在应用中重复使用的无状态可组合项。

在您从可组合函数中提取状态后,生成的可组合函数称为无状态函数。也就是说,通过从可组合函数中提取状态,可以将其变为无状态。

无状态可组合项是指没有状态的可组合项,这意味着它不会保存、定义或修改新状态。相反,有状态可组合项是指具有可以随时间变化的状态的可组合项。

状态提升是一种将状态移到其调用方以使组件变为无状态的模式。

当应用于可组合项时,这通常意味着向可组合项引入以下两个形参:

  • value: T 形参,即要显示的当前值。
  • onValueChange: (T) -> Unit - 回调 lambda,会在值更改时触发,以便可以在其他位置更新状态(例如,当用户在文本框中输入一些文本时)。

EditNumberField() 函数中提升状态:

  1. 更新 EditNumberField() 函数定义,以通过添加 valueonValueChange 形参来提升状态:
@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
//...

value 形参的类型为 StringonValueChange 形参的类型为 (String) -> Unit,因此它是一个接受 String 值作为输入且没有返回值的函数。onValueChange 形参用作传入 TextField 可组合项的 onValueChange 回调。

  1. EditNumberField() 函数中,更新 TextField() 可组合函数以使用传入的形参:
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. 提升状态,将记住的状态从 EditNumberField() 函数移至 TipTimeLayout() 函数:
@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)
  
   Column(
       //...
   ) {
       //...
   }
}
  1. 您已将状态提升到 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
)
  1. 在函数 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 属性计算并向用户显示小费金额。

  1. 在模拟器或设备上运行应用,然后在 Bill Amount 文本框中输入一个值。系统即会显示小费金额(账单金额的 15%),如下图所示:

de593783dc813e24.png

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 会在首次执行可组合函数时创建界面。
  • 重组过程会再次运行相同的可组合项,以便在其数据发生变化时更新树。
  • 状态提升是一种将状态移到其调用方以使组件变为无状态的模式。

了解更多内容