Compose 中的状态简介

1. 开始之前

本 Codelab 教您了解状态,以及如何在 Jetpack Compose 中使用和操作状态。

从本质上讲,应用程序中的状态是任何随着时间推移而可能发生变化的值。此定义非常广泛,涵盖从数据库到应用程序中的变量的所有内容。您将在后面的单元中详细了解数据库,但现在您只需要知道数据库是结构化信息的组织集合,例如您计算机上的文件。

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

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

在本 Codelab 中,您将探讨如何在使用 Compose 时使用和思考状态。为此,您将构建一个名为 Tip Time 的小费计算器应用程序,该应用程序使用以下内置 Compose UI 元素:

  • 一个 TextField 可组合函数,用于输入和编辑文本。
  • 一个 Text 可组合函数,用于显示文本。
  • 一个 Spacer 可组合函数,用于在 UI 元素之间显示空白。

在本 Codelab 结束时,您将构建一个交互式小费计算器,该计算器在您输入服务金额时会自动计算小费金额。此图片显示了最终应用程序的外观

e82cbb534872abcf.png

先决条件

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

您将学到什么

  • 如何在 UI 中思考状态。
  • Compose 如何使用状态来显示数据。
  • 如何在应用程序中添加文本框。
  • 如何提升状态。

您将构建什么

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

您需要什么

  • 一台连接互联网并配备网页浏览器的计算机
  • Kotlin 知识
  • 最新版本的 Android Studio

2. 开始

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

46bf4366edc1055f.png 18da3c120daa0759.png

  1. 账单小费 % 框中输入不同的值。小费和总计值会发生变化。

c0980ba3e9ebba02.png

请注意,您输入值的那一刻,小费总计就会更新。在本 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 代码库中浏览入门代码。

入门应用程序概述

为了熟悉入门代码,请完成以下步骤:

  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() 函数。这将在设计窗格和拆分窗格中显示应用程序的布局。

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

ae11354e61d2a2b9.png

获取用户的输入

在本节中,您将添加允许用户在应用程序中输入账单金额的 UI 元素。您可以看到它在此图片中的样子

58671affa01fb9e1.png

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

样式和主题是属性集合,指定单个 UI 元素的外观。样式可以指定属性,例如字体颜色、字体大小、背景颜色等等,这些属性可以应用于整个应用程序。后面的 Codelab 将介绍如何在应用程序中实现这些功能。现在,这些功能已经为您完成,以使您的应用程序更美观。

为了更好地理解,以下是对带有和不带自定义主题的解决方案版本的应用程序的并排比较。

没有自定义主题。

带有自定义主题。

TextField 可组合函数允许用户在应用程序中输入文本。例如,请注意此图片中 Gmail 应用程序登录屏幕上的文本框

Phone screen with gmail app with a text field for email

向应用程序添加 TextField 可组合函数

  1. MainActivity.kt 文件中,添加一个 EditNumberField() 可组合函数,该函数接受一个 Modifier 参数。
  2. EditNumberField() 函数主体中,在 TipTimeLayout() 之后,添加一个 TextField,它接受一个名为 value 的参数,该参数设置为一个空字符串,以及一个名为 onValueChange 的参数,该参数设置为一个空 lambda 表达式
@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. 设计窗格中,您应该看到 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. 组合

应用程序中的可组合项描述了一个 UI,该 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 中使用 StateMutableState 类型来使应用程序中的状态可被 Compose 观察或跟踪。 State 类型是不可变的,因此您只能读取其中的值,而 MutableState 类型是可变的。您可以使用 mutableStateOf() 函数来创建可观察的 MutableState。它接收一个作为参数的初始值,该值被包装在一个 State 对象中,然后使其 value 可观察。

mutableStateOf() 函数返回的值

  • 持有状态,即账单金额。
  • 是可变的,因此可以更改值。
  • 是可观察的,因此 Compose 会观察值的任何更改并触发重新组合以更新 UI。

添加服务成本状态

  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 变量更新为新值。 amountInput 状态被 Compose 跟踪,因此只要它的值发生更改,就会安排重新组合,并且会再次执行 EditNumberField() 可组合函数。在该可组合函数中, amountInput 变量将重置为其初始的 0 值。因此,文本框显示 0 值。

使用您添加的代码,状态更改会导致安排重新组合。

但是,您需要一种方法来跨重新组合保存 amountInput 变量的值,以便它不会在每次 EditNumberField() 函数重新组合时都重置为 0 值。您将在下一节中解决此问题。

6. 使用 remember 函数保存状态

可组合方法可以被多次调用,因为会发生重新组合。如果可组合项的状态没有被保存,那么它会在重新组合时重置其状态。

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

EditNumberField() 函数中使用 remember 函数

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

现在,空字符串是 amountInput 变量的初始默认值。 by 是一个 Kotlin 属性委托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. EditNumberField() 函数中,在 onValueChange 命名参数旁边,设置一个行断点。
  2. 在导航菜单中,单击调试“app”。应用程序将在模拟器或设备上启动。当 TextField 元素创建时,应用程序的执行将首次暂停。

154e060231439307.png

  1. 在 **调试** 窗格中,单击 2a29a3bad712bec.png **恢复程序**。文本框已创建。
  2. 在模拟器或设备上,在文本框中输入一个字母。当您的应用到达您设置的断点时,它会再次暂停执行。

当您输入文本时,onValueChange 回调函数会被调用。在 lambda it 中包含您在键盘上输入的新值。

将 “it” 的值分配给 amountInput 后,Compose 会使用新数据触发重新合成,因为可观察值已更改。

1d5e08d32052d02e.png

  1. 在 **调试** 窗格中,单击 2a29a3bad712bec.png **恢复程序**。在模拟器或设备上输入的文本显示在包含断点的行旁边,如本图所示。

1f5db6ab5ca5b477.png

这是文本字段的状态。

  1. 单击 2a29a3bad712bec.png **恢复程序**。输入的值将显示在模拟器或设备上。

8. 修改外观

在上一节中,您让文本字段正常工作。在本节中,您将增强 UI。

向文本框添加标签

每个文本框都应有一个标签,让用户知道他们可以输入什么信息。在下例图像的第一部分中,标签文本位于文本字段的中间,并与输入行对齐。在下例图像的第二部分中,当用户单击文本框以输入文本时,标签会在文本框中移到更高位置。要了解有关文本字段结构的更多信息,请参阅 结构

a2afd6c7fc547b06.png

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

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

这将文本框从多行压缩为单行,并且可以水平滚动。

  1. 添加 keyboardOptions 参数,并将其设置为 KeyboardOptions()
import androidx.compose.foundation.text.KeyboardOptions

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

Android 提供了一个选项,用于配置屏幕上显示的键盘,以便输入数字、电子邮件地址、URL 和密码等等。要了解有关其他键盘类型的更多信息,请参阅 KeyboardType

  1. 将键盘类型设置为数字键盘,以便输入数字。将 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
    )
}
  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 变量之后,创建一个名为 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)
   )
}

显示计算的小费金额

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

  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 变量。您需要将 amount 变量公开给 TipTimeLayout() 函数。此图像说明了所需的代码结构,该结构使 EditNumberField() 可组合框变为无状态

ab4ec72388149f7c.png

此模式称为状态提升。在下一节中,您将提升提取可组合框中的状态,使其变为无状态。

10. 状态提升

在本节中,您将学习如何决定在何处定义状态,以便您可以重复使用和共享可组合框。

在可组合函数中,您可以定义变量来保存要显示在 UI 中的状态。例如,您在 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
) {
//...

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.

  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 无状态。您已将 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
)
  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. 在模拟器或设备上运行应用,然后在“账单金额”文本框中输入一个值。如您在此图像中所见,将显示账单金额的 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 在执行可组合函数时构建的 UI 的描述。Compose 应用调用可组合函数将数据转换为 UI。
  • 初始组合是 Compose 在第一次执行可组合函数时创建 UI 的过程。
  • 重新组合是重新运行相同可组合函数以在它们的数据更改时更新树的过程。
  • 状态提升是一种将状态移至其调用者的模式,以使组件无状态。

了解更多