Compose 中的状态简介

1. 开始之前

此 Codelab 将教你了解状态,以及 Jetpack Compose 如何使用和操作状态。

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

所有 Android 应用都向用户显示状态。Android 应用中状态的一些示例包括

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

在此 Codelab 中,您将探索在使用 Compose 时如何使用和思考状态。为此,您将构建一个名为 Tip Time 的计算器应用,其中包含以下内置的 Compose UI 元素

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

在本 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. 在“设计”窗格中,您应该会看到“计算小费”文本、一个空文本框和“小费金额”文本可组合函数。

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,它显示一个包含一些文本、间隔符和文本框的列。文本显示 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 的状态(即 amountInput 变量),当 TextField 通知您文本发生更改时,通过 onValueChange 回调函数。

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

3a2c62f8ec55e339.gif

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

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

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

6. 使用 remember 函数保存状态

可组合方法可以由于重新组合而被多次调用。如果可组合函数的状态未保存,则它会在重新组合期间重置其状态。

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

EditNumberField() 函数中使用 remember 函数

  1. EditNumberField() 函数中,使用 by remember Kotlin 属性委托初始化 amountInput 变量,方法是用 remember 括起对 mutableStateOf() 函数的调用。
  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 变量之后,创建另一个名为 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 变量。您需要将 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
) {
//...

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 成为无状态的。您已将 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 的过程。
  • 重新组合是指在数据发生变化时再次运行相同的可组合函数以更新树的过程。
  • 状态提升是一种将状态移动到其调用者以使组件成为无状态的模式。

了解更多