Jetpack Compose(第七趴)——Jetpack Compose 中的状态(上)

1,511 阅读8分钟

通过这一趴,你将学习到

  • 如何看待Jetpack Compose界面中的状态和事件?
  • Compose如何使用状态确定要在屏幕上显示的元素?
  • 什么是状态提升?
  • 有状态可组合函数和无状态可组合函数如何运作?
  • Compose如何使用State<T>API自动跟踪状态?
  • 内存和内部状态如何用于可组合函数中?使用rememberrememberSaveableAPI
  • 如何使用列表和状态?使用mutableStateListOftoMutableStateListAPI
  • 如何结合使用viewModel与Compose?

最终构建一个简单的健康应用:

image.png

该应用具有两项主要功能:

  • 用于跟踪饮水量和饮水计数器
  • 全天内的待完成健康任务列表

一、进行设置

  1. 如需启动新的Compose项目,请打开Android Studio。
  2. 如果您位Welcome to Android Studio窗口,请点击Start a new Android Studio project。如果您已打开Android Studio项目,请从菜单栏中依次选择File>New>New Project
  3. 对于新项目,请从可用模版中选择Empty Compose Activity

image.png

4. 点击**Next**,然后配置项目并将其命名为“**BasicStateCodelab**”。

请确保您选择的minimumSdkVersion至少为API级别21,这是Compose支持的最低API级别。

当您选择Empty Compose Activity模版时,Android Studio会在您的项目中完成以下设置:

  • MainActivity类,其中配置的可组合函数可在屏幕上显示一些文本。
  • AndroidMainfest.xml文件,用于定义应用的权限、组件和自定义资源。
  • build.gradleapp/build.gradle文件包含Compose所需的选项和依赖项。

二、Compose中的状态

应用的“状态”是指可以随时间变化的任何值。这是一个非常宽泛的定义,从Room数据库到类的变量,全部涵盖在内。

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

  • 聊天应用中最新接收的消息。
  • 用户的个人资料照片。
  • 在项列表中的滚动位置。

现在开始编写健康应用。 您需要构建的第一项功能是饮水计时器,用于记录您一天喝了多少杯水。

创建一个名为WaterCounter的可组合函数,其中包含一个Text可组合项,用于显示饮水杯数。饮水杯数应该存储在名为count的值中,您现在可以对其进行硬编码。

创建一个包含WaterCounter可组合函数的新文件WaterCounter.kt,如下所示:

import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    val count = 0
    Text(
        text = "You're had $count glasses.",
        modifier = modifier.padding(16.dp)
    )
}

让我们创建一个表示整个屏幕的可组合函数,该函数将包括两个部分,即饮水计时器和健康任务列表。现阶段,我们只需添加计数器。

  1. 创建一个代表主屏幕的文件WellnessScreen.kt,然后调用WaterCount函数:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modfier = Modifier) {
    WaterCounter(modifier)
}
  1. 打开MainActivity.kt。移出GreetingDefaultPreview可组合项。在activity的setContent块内调用新创建的WellnessScreen可组合项,如下所示:
class MainActivity: ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicStateCodelabTheme {
                // A surface container using the 'backgtoung' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    WellnessScreen()
                }
            }
        }
    }
}
  1. 如果现在运行应用,您会看到我们的基本饮水计数器屏幕,其中包含硬编码的饮水杯数。

image.png

WaterCounter可组合函数的状态为变量count。但是,静态状态无法修改,因此用处不大。如需解决此问题,请添加Button以增加计数ing跟踪全天的饮水杯数。

任何会导致状态修改的操作都称为“事件”。

三、Compose中的事件

“状态”是指可以随时间变化的任何只,例如,聊天应用最新收到的消息。但是,是什么原因导致状态更新呢?在Android应用中,状态会根据事件进行更新。

事件是从应用外部或内部生成的输入,例如:

  • 用户与界面互动,例如按下按钮。
  • 其他因素,例如传感器发送新值或网络响应。

应用的状态说明了要在界面中显示的内容,而时间则是一种机制,可在状态发生变化时导致界面发生变化。

事件用于通知程序发生了某事。所有Android应用都有核心界面更新循环,如下所示:

image.png

  • 事件:由用户或程序的其他部分生成。
  • 更新状态:事件处理脚本会更改界面所使用的状态。
  • 显示状态:界面会更新以显示新状态。

Compose中的状态管理主要是了解状态和事件之间的交互方式。

现在,添加按钮以便用户通过添加更多饮水杯数来修改状态。

找到WaterCounter可组合函数,将Button添加到标签Text的下方。Column可帮助您垂直对齐TextButton可组合项。您可以将外部内边距移至Column可组合项,并在Button的顶部添加一些额外的内边距,使其与Text分离。

Button可组合函数接收onClicklambda函数 用户点击按钮时会发生此事件。稍后您会看到更多lambda函数示例。

count更改为var(而不是val),使其具有可变性。

import androidx.compose.material.Button
import androidx.compose.foundation.layout.Column

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count = 0
        Text("You're had $count glasses.")
        Button(onClick = { count ++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

运行应用并点击按钮时,您会发现没有任何反应。为count变量设置不同的值不会使Compose将其检测为状态更改。因此不会产生任何效果。这是因为,当状态发生变化时,您尚未指示Compose应重新绘制屏幕(即“重组”可组合函数)。您将在接下来的步骤中解决此问题。

1.gif

四、可组合函数中的记忆功能

Compose应用通过可组合函数将数据转换为界面。组合是指Compose在执行可组合项时构建的界面描述。如果发生状态更改,Compose会使用新状态重新执行受影响的可组合函数,从而创建更新后的界面。这一过程称为重组。Compose还会查看各个可组合项需要哪些数据,而避免重组未受影响的组件。

组合:Jetpack Compose在执行可组合项构建的界面描述。 初始组合:通过首次运行可组合项创建组合。 重组:在数据发生变化时重新运行可组合项以更新组合。

为此,Compose需要知道要跟踪的状态,以便在收到更新时安排重组,

Compose采用特殊的状态跟踪系统,可以为读取特定状态的任何可组合项安排重组。 这让Compose能够实现精细控制,并且仅重组需要更改的可组合函数,而不是重组整个界面。这将通过同时跟踪针对状态的“写入”(即状态变化)和针对状态的“读取”来实现。

使用Compose的StateMutableState类型让Compose能够观察到状态。

Compose会跟踪每个读取状态value属性的可组合项,并在其value更改时触发重组。您可以使用mutableStateOf函数来创建可观察的MutableState。它接受初始值作为封装在State对象中的参数,这样便可使其value变为可观察。

更新WaterCounter可组合项,以便count0为初始值来使用mutableStateOfAPI。当mutableStateOf返回MutableState类型时,您可以更新其value以更新状态,并且Compose会在其value被读取时触发这些函数的重组。

import androidx.compose.runtime.MutableState
import androidx.composr.runtime.mutableStateOf

@Composable
fun WaterCount(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        // Changes to count are now tracked by Compose
        val count: MutableState<Int> = mutableStateOf(0)
        
        Text("You've had @{count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

image.png

如前所述,对count所做的任何改变都会安排对自动重组读取countvalue的所有可组合函数进行重组。在此情况下,点击按钮即会触发重组WaterCounter

如果现在运行应用,您会再次发现没没有发生任何变化!

1.gif

安排重组的过程没有问题。不过,当重组发生时,变量count会重新初始化为0,因此我们需要通过某种方式在重组后保留此值。

您可以将remember视为一种在组合中存储单个对象的机制,就像私有val属性在对象中执行的操作一样。

remembermutableStateOf通常在可组合函数中一起使用。

修改WaterCounter,将对mutableStateOf的调用置于remember内嵌可组合函数的内部。

import androidx.compose.runtime.remember

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState<Int> = remember { mutableStateOf(0) }
        Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

或者,我们也可以使用Kotlin的委托属性来简化count的使用。

您可以使用关键字bycount定义为var。通过添加委托的getter和setter导入内容,我们可以间接读取count并将其设置为可变,而无需每次都显示引用MutableStatevalue属性。

现在,WaterCounter如下所示:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }
        
        Text("You've had $count glasses.")
        Button(onClick = { count++ }, Modifier.paddint(top = 8.dp)) {
            Text("Add one")
        }
    }
}

您选择的语法应该能够在您编写的可组合项中生成可持续性最高的代码。

我们来回顾一下到目前为止的成果:

  • 定义了一个持续记忆的变量,名称为count
  • 创建了一个文本显示区,其中向用户呈现记忆的数字,
  • 添加了一个按钮,每次点击该按钮都会导致记忆的数字递增。

这种安排可与用户形成数据流反馈循环:

  • 界面向用户显示状态(当前计数显示为文本)。
  • 用户生成的事件会与现有状态合并以生成新状态(点击按钮会为当前计数加一)

您的计数器已就绪且可正常运行!

2.gif

五、状态驱动型截面

Compose是一个个声明性界面框架。它描述界面在特定状况下的状态,而不是在状态发生变化时移除界面组件或更改其可见性。调用重组并更新界面后,可组合项最终可能会进入或退出组合。

image.png

此方法可避免像针对视图系统系统那样手动更新视图的复杂性。这也太不容易出错,因为您不会忘记根据新状态更新视图,因为系统会自动执行此过程。

如果在初始组合期间和重组期间调用了可组合函数,则认为其存在于组合中。未调用的可组合函数(例如,由于该函数在if语句内调用且未满足条件)不存在于组合中。

组合的输出是描述界面的树结构。

Android Studio的布局检查器工具可用于检查Compose生成的应用布局。我们接下来将执行此操作。

为了掩饰此过程,请修改代码,以根据状态显示界面。打开WaterCounter,如果count大于0,则显示Text

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier: modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }
        
        if (count > 0) {
            // This text is present if the button has been clicked
            // at least once; absent otherwise
            Text("You've had $count glasses.")
        }
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

运行应用,然后依次选择Tool>Layout Inspector,打开Android Studio的布局检查器工具。

您会看到一个分屏:左侧是组件树,右侧是应用预览。 点按屏幕左侧的根元素BasicStateCodelabTheme可浏览树。点击"Expand all"按钮,展示整个组件树。

点击屏幕右侧的某个元素即可访问树的相应元素。

image.png

如果您按应用上的Add one按钮:

  • 计数增加到1且状态发生变化。
  • 系统调用重组。
  • 屏幕使用新元素重组。

现在,使用Android Studio的布局检查器工具检查组件树,您还会看到Text可组合项:

image.png

界面的不同部分可以以来相同的状态。修改Button,使其中count达到10之前处于启动状态,并在达到10之后停用(即您达到当天的目标)。为此,请使用Buttonenabled参数。

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    ...
        Button(onClick = { count ++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
    ...
}

现在运行应用。对count状态的更改决定是否显示Text,以及是启用还是停用Button

3.gif