Jetpack Compose中的state核心思想

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中的state核心思想相关的知识,希望对你有一定的参考价值。

Compose 中的状态

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

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

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

关键提示:状态决定界面在任何特定时间的显示内容。

下面的示例通过构建一个健康App应用来展示如何正确的使用Compose中的状态。

构建的第一项功能是饮水计数器,用于记录您一天饮用了多少杯水。

创建一个名为 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've had $count glasses.",
       modifier = modifier.padding(16.dp)
   )
   Button(onClick =  count++ , Modifier.padding(top = 8.dp)) 
       Text("Add one")
   

创建一个代表主屏幕的文件 WellnessScreen.kt,然后调用 WaterCounter 函数:

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) 
   WaterCounter(modifier)

打开 MainActivity.kt,在 activity 的 setContent 块内调用新创建的 WellnessScreen 可组合项,如下所示:

class MainActivity : ComponentActivity() 
   override fun onCreate(savedInstanceState: Bundle?) 
       super.onCreate(savedInstanceState)
       setContent 
           BasicStateCodelabTheme 
               // A surface container using the 'background' color from the theme
               Surface(
                   modifier = Modifier.fillMaxSize(),
                   color = MaterialTheme.colors.background
               ) 
                   WellnessScreen()
               
           
       
   

提示:setContent 内部的 MainActivity 中使用的应用主题取决于项目名称。此处假定项目名为 BasicStateCodelab。

如果现在运行应用,您会看到我们的基本饮水计数器屏幕,其中包含硬编码的饮水杯数。

WaterCounter 可组合函数的状态为变量 count。但是,点击按钮时,您会发现没有任何反应。为 count 变量设置不同的值不会使 Compose 将其检测为状态更改,因此不会产生任何效果。这是因为,当状态发生变化时,您尚未指示 Compose 应重新绘制屏幕(即“重组”可组合函数)。

可组合函数中的记忆功能

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

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

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

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

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

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

现在修改代码如下:

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

@Composable
fun WaterCounter(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")
       
   

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

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

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

为此,我们可以使用 remember 可组合内嵌函数。系统会在初始组合期间将由 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")
        
    

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

这里还可以使用 Kotlin 的委托属性来简化 count 的使用,通过关键字 by 将 count 定义为 var。通过添加委托的 getter 和 setter 导入内容,我们可以间接读取 count 并将其设置为可变,而无需每次都显式引用 MutableState 的 value 属性。

修改代码如下:

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.padding(top = 8.dp)) 
           Text("Add one")
       
   

现在运行应用,您的计数器已就绪且可正常运行!

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

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

状态驱动型界面

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

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

如果在初始组合期间或重组期间调用了可组合函数,则认为其存在于组合中。未调用的可组合函数(例如,由于该函数在 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")
       
   

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

提醒:如需在检查器中查看 Compose 节点,请使用 API 大于或等于 29 的设备

您会看到一个分屏:左侧是组件树,右侧是应用预览。

点按屏幕左侧的根元素 BasicStateCodelabTheme 可浏览树。点击 Expand all 按钮,展开整个组件树。

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

此时如果点击应用上的 Add one 按钮:

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

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

状态驱动界面在给定时刻显示哪些元素。

界面的不同部分可以依赖于相同的状态。修改 Button,使其在 count 达到 10 之前处于启用状态,并在达到 10 之后停用(即您达到当天的目标)。

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

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

组合中的记忆功能

remember 会将对象存储在组合中,而如果在重组期间未再次调用之前调用 remember 的来源位置,则会忘记对象。

为了直观呈现这种行为,我们将在应用中实现以下功能:当用户至少饮用了一杯水时,向用户显示有一项待执行的健康任务,同时用户也可以关闭此任务。由于可组合项应较小并可重复使用,因此请创建一个名为 WellnessTaskItem 的新可组合项,该可组合项根据以参数形式接收的字符串来显示健康任务,并显示一个 Close 图标按钮。

创建一个新文件 WellnessTaskItem.kt,并添加以下代码。

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

@Composable
fun WellnessTaskItem(
    taskName: String,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) 
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) 
        Text(
            modifier = Modifier.weight(1f).padding(start = 16.dp),
            text = taskName
        )
        IconButton(onClick = onClose) 
            Icon(Icons.Filled.Close, contentDescription = "Close")
        
    

WellnessTaskItem 函数会接收任务说明和 onClose lambda 函数(就像内置 Button 可组合项接收 onClick 一样)。

WellnessTaskItem 如下所示:

接下来为应用添加更多功能,请更新 WaterCounter,以在 count > 0 时显示 WellnessTaskItem。

当 count 大于 0 时,定义一个变量 showTask,用于确定是否显示 WellnessTaskItem 并将其初始化为 true。

添加新的 if 语句,以在 showTask 为 true 时显示 WellnessTaskItem。使用前面介绍的 remember 来确保 showTask 值在重组后继续有效。

@Composable
fun WaterCounter() 
   Column(modifier = Modifier.padding(16.dp)) 
       var count by remember  mutableStateOf(0) 
       if (count > 0) 
           var showTask by remember  mutableStateOf(true) 
           if (showTask) 
               WellnessTaskItem(
                   onClose =  ,
                   taskName = "Have you taken your 15 minute walk today?"
               )
           
           Text("You've had $count glasses.")
       

       Button(onClick =  count++ , enabled = count < 10) 
           Text("Add one")
       
   

使用 WellnessTaskItem 的 onClose lambda 函数实现:在按下 X 按钮时,变量 showTask 更改为 false,且不再显示任务。

   ...
   WellnessTaskItem(
      onClose =  showTask = false ,
      taskName = "Have you taken your 15 minute walk today?"
   )
   ...

接下来,添加一个带“Clear water count”文本的新 Button,并将其放置在“Add one”Button 旁边。按下“Clear water count”按钮后,变量 count 会重置为 0。

WaterCounter 可组合函数应如下所示。

import androidx.compose.foundation.layout.Row

@Composable
fun WaterCounter(modifier: Modifier = Modifier) 
   Column(modifier = modifier.padding(16.dp)) 
       var count by remember  mutableStateOf(0) 
       if (count > 0) 
           var showTask by remember  mutableStateOf(true) 
           if (showTask) 
               WellnessTaskItem(
                   onClose =  showTask = false ,
                   taskName = "Have you taken your 15 minute walk today?"
               )
           
           Text("You've had $count glasses.")
       

       Row(Modifier.padding(top = 8.dp)) 
           Button(onClick =  count++ , enabled = count < 10) 
               Text("Add one")
           
           Button(onClick =  count = 0 , Modifier.padding(start = 8.dp)) 
               Text("Clear water count")
           
       
   

运行应用时,屏幕会显示初始状态:

右侧是简化版组件树,可帮助您分析状态发生变化时会发生什么情况。count 和 showTask 是记住的值。

现在,您可以在应用中按以下步骤操作:

按下 Add one 按钮。此操作会递增 count(这会导致重组),并同时显示 WellnessTaskItem 和计数器 Text。


此时点击 WellnessTaskItem 组件的 X(这会导致另一项重组)。showTask 现在为 false,这意味着不再显示 WellnessTaskItem。

此时点击 Add one 按钮(另一项重组)。如果您继续增加杯数,showTask 会记住您在下一次重组时关闭了 WellnessTaskItem。

此时点击 Clear water count 按钮可将 count 重置为 0 并导致重组。系统不会调用显示 count 的 Text 以及与 WellnessTaskItem 相关的所有代码,并且会退出组合。

由于系统未调用之前调用 showTask 的代码位置,因此会忘记 showTask。这将返回第一步。

此时点击 Add one 按钮,使 count 大于 0(重组)。

系统再次显示 WellnessTaskItem 可组合项,因为在退出上述组合时,之前的 showTask 值已被忘记。

如果我们要求 showTask 在 count 重置为 0 之后持续保留超过 remember 允许的时间(也就是说,即使重组期间未调用之前调用 remember 的代码位置),会发生什么?在接下来的部分中,我们将探讨如何修正这些问题以及更多示例。

现在,您已经了解了界面和状态在退出组合后的重置过程,请清除代码并返回到本部分开头的 WaterCounter:

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

在 Compose 中恢复状态

运行应用,为计数器增加一些饮水杯数,然后旋转设备。请确保已为设备启用自动屏幕旋转设置。

由于系统会在配置更改后(在本例中,即改变屏幕方向)重新创建 activity,因此已保存状态会被忘记:计数器会在重置为 0 后消失。

如果您更改语言、在深色模式与浅色模式之间切换,或者执行任何导致 Android 重新创建运行中 activity 的其他配置更改时,也会发生相同的情况。

虽然 remember 可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用 rememberSaveable,而不是 remember

rememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。(如需了解详细请参考如何在 Compose 中恢复状态文档。)

在 WaterCounter 中,将 remember 替换为 rememberSaveable

import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun WaterCounter(modifier: Modifier = Modifier) 
        ...
        var count by rememberSaveable  mutableStateOf(0) 
        ...

现在运行应用并尝试进行一些配置更改。您应该会看到计数器已正确保存。

重新创建 activity 只是 rememberSaveable 的用例之一。我们稍后会在使用列表时探索另一个用例。

在重新创建 activity 或进程后,您可以使用 rememberSaveable 恢复界面状态。除了在重组后保持状态之外,rememberSaveable 还会在重新创建 activity 和进程之后保留状态。

请根据应用的状态和用户体验需求来考虑是使用 remember 还是 rememberSaveable。

状态提升

使用 remember 存储对象的可组合项包含内部状态,这会使该可组合项有状态。在调用方不需要控制状态,并且不必自行管理状态便可使用状态的情况下,“有状态”会非常有用。但是,具有内部状态的可组合项往往不易重复使用,也更难测试。

不保存任何状态的可组合项称为无状态可组合项。如需创建无状态可组合项,一种简单的方法是使用状态提升

Compose 中的状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。Jetpack Compose 中的常规状态提升模式是将状态变量替换为两个参数:

  • value: T:要显示的当前值
  • onValueChange: (T) -> Unit:请求更改值的事件,其中 T 是建议的新值

其中,value值表示任何可修改的状态。

状态下降、事件上升的这种模式称为单向数据流 (Unidirectional Data Flow, UDF),而状态提升就是我们在 Compose 中实现此架构的方式。如需了解相关详情,请参阅 Compose 架构文档

以这种方式提升的状态具有一些重要的属性:

  • 单一可信来源:通过移动状态,而不是复制状态,我们可确保只有一个可信来源。这有助于避免 bug。
  • 可共享:可与多个可组合项共享提升的状态。
  • 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
  • 解耦:无状态可组合函数的状态可以存储在任何位置。例如,存储在 ViewModel 中。
  • 封装:只有有状态可组合项能够修改其状态。这完全是内部的。

请尝试为 WaterCounter 实现状态提升,以便从以上所有方法中受益。

有状态与无状态

当所有状态都可以从可组合函数中提取出来时,生成的可组合函数称为无状态函数。

无状态可组合项是指不具有任何状态的可组合项,这意味着它不会存储、定义或修改新状态。
有状态可组合项是一种具有可以随时间变化的状态的可组合项。
在实际应用中,让可组合项 100% 完全无状态可能很难实现,具体取决于可组合项的职责。在设计可组合项时,您应该让可组合项拥有尽可能少的状态,并能够在必要时通过在可组合项的 API 中公开状态来提升状态。

重构 WaterCounter 可组合项,将其拆分为两部分:有状态和无状态计数器。

StatelessCounter 的作用是显示 count,并在您递增 count 时调用函数。为此,请遵循上述模式并传递状态 count(作为可组合函数的参数)和 lambda (onIncrement)(在需要递增状态时会调用此函数)。StatelessCounter 如下所示:

@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) 
   Column(modifier = modifier.padding(16.dp)) 
       if (count > 0) 
           Text("You've had $count glasses.")
       
       Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) 
           Text("Add one")
       
   

StatefulCounter 拥有状态。这意味着,它会存储 count 状态,并在调用 StatelessCounter 函数时对其进行修改:

@Composable
fun StatefulCounter(modifier:<

以上是关于Jetpack Compose中的state核心思想的主要内容,如果未能解决你的问题,请参考以下文章

Jetpack Compose State:修改类属性

Jetpack Compose 深入探索系列五:State Snapshot System

如何在 Jetpack compose 中制作可重用的组件?

Jetpack Compose LazyColumn 以编程方式滚动到项目

Jetpack Compose 中的架构思想

如何从 Jetpack Compose TextField 关闭虚拟键盘?