状态以及 Jetpack Compose 如何使用和操作状态。
在我们深入研究之前,定义状态到底是什么很有用。 从本质上讲,应用程序中的状态是任何可以随时间变化的值。 这是一个非常广泛的定义,包括从 Room 数据库到类上的变量的所有内容。
目标:
什么是单向数据流
如何在 UI 中考虑状态和事件
如何在 Compose 中使用 Architecture Component 的 ViewModel 和 LiveData 来管理状态
Compose 如何使用状态来绘制屏幕
何时将状态移动到调用者
如何在 Compose 中使用内部状态
如何使用 State<T> 将状态与 Compose 集成
使用单向数据流
为了帮助解决非结构化状态的这些问题,我们引入了包含 ViewModel 和 LiveData 的 Android 架构组件。
ViewModel 允许您从 UI 中提取状态并定义 UI 可以调用以更新该状态的事件
官方的例子理解原理:
ViewModel 还公开了一个事件:onNameChanged。 此事件由 UI 调用以响应用户事件,例如每当 EditText 的文本更改时此处会发生什么。
回到我们之前讨论过的 UI 更新循环,我们可以看到这个 ViewModel 如何与事件和状态结合在一起。
事件 – onNameChanged 在文本输入更改时由 UI 调用
更新状态 - onNameChanged 进行处理,然后设置 _name 的状态
显示状态 - 调用名称的观察者,通知 UI 状态变化
通过以这种方式构建我们的代码,我们可以认为事件“向上”流向 ViewModel。 然后,为了响应事件,ViewModel 将进行一些处理并可能更新状态。 当状态更新时,它会“向下”流向
这种模式称为单向数据流。 单向数据流是一种状态向下流动而事件向上流动的设计。 通过以这种方式构建我们的代码,我们获得了一些优势:
- 可测试性——通过将状态与显示它的 UI 分离,可以更轻松地测试 ViewModel 和 Activity
- 状态封装——因为状态只能在一个地方(ViewModel)更新,随着 UI 的增长,你不太可能引入部分状态更新错误
- UI 一致性——所有状态更新都通过使用可观察状态持有者立即反映在 UI 中
因此,虽然这种方法确实添加了更多代码,但使用单向数据流处理复杂的状态和事件往往更容易、更可靠。
单向数据流是一种事件向上流动而状态向下流动的设计。
例如,在 ViewModel 中,事件通过来自 UI 的方法调用传递,而状态使用 LiveData 向下流动。
它不仅仅是描述 ViewModel 的术语——任何事件向上流动和状态下降的设计都是单向的。
如何使用 ViewModel 在 Compose 中使用单向数据流。
上面理论,使用 ViewModel 和 LiveData 探索了 Android View 系统中的单向数据流。
StateCodeLab 项目 解读
TodoScreen.kt – 这些可组合项直接与状态交互,我们将在探索 compose 状态时编辑此文件。
TodoComponents.kt – 这些可组合定义了我们将用于构建 TodoScreen 的可重用 UI 位。 您无需编辑这些可组合项即可完成此 Codelab。
这种文件划分有点随意,将 TodoScreen.kt 中的代码集中在状态上。 在实践中,这些组合项可能位于同一个文件中,或者分布在多个文件中,具体取决于您在项目中如何使用它们。
-
TodoScreen
函数
这个可组合显示一个可编辑的 TODO 列表,但它没有任何自己的状态。 请记住,状态是任何可以更改的值——但 TodoScreen 的任何参数都不能修改。
items – 要显示在屏幕上的不可变项目列表
onAddItem – 用户请求添加项目时的事件
onRemoveItem – 用户请求删除项目时的事件
事实上,这个可组合是无状态的。 它只显示传入的项目列表,无法直接编辑列表。 相反,它传递了两个可以请求更改的事件 onRemoveItem 和 onAddItem。
这就提出了一个问题:如果它是无状态的,它如何显示可编辑列表? 它通过使用一种称为状态提升的技术来做到这一点。 状态提升是向上移动状态以使组件无状态的模式。 无状态组件更容易测试,往往有更少的错误,并提供更多的重用机会。
事件 – 当用户请求添加或删除项目时 TodoScreen 调用 onAddItem 或 onRemoveItem
更新状态——TodoScreen 的调用者可以通过更新状态来响应这些事件
显示状态 - 当状态更新时,TodoScreen 将使用新项目再次调用,并且可以在屏幕上显示它们
调用者负责确定在何处以及如何保持此状态。 它可以存储项目但有意义,例如在内存中或从 Room 数据库中读取它们。 TodoScreen 与状态的管理方式完全分离。
使用这个 ViewModel 从 TodoScreen 提升状态。 完成后,我们将创建一个单向数据流设计
TodoScreen 集成到 TodoActivity.kt
class TodoActivity : AppCompatActivity() {
val todoViewModel by viewModels<TodoViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StateCodelabTheme {
Surface {
TodoActivityScreen(todoViewModel)
}
}
}
}
}
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
TodoScreen(
items = todoViewModel.todoItems,
currentlyEditing = todoViewModel.currentEditItem,
onAddItem = todoViewModel::addItem,
onRemoveItem = todoViewModel::removeItem,
onStartEdit = todoViewModel::onEditItemSelected,
onEditItemChange = todoViewModel::onEditItemChange,
onEditDone = todoViewModel::onEditDone
)
}
Android Studio 在启动新 Compose 项目时创建的默认主题。
Surface 为应用程序添加背景,并配置文本颜色。
Flow the events up 事件向上流动
Kotlin 提示
您还可以使用方法引用语法生成一个调用单个方法的 lambda。 这将从方法调用中创建一个 lambda。 使用方法引用语法,上面的 onAddItem 也可以表示为 onAddItem = todoViewModel::addItem。
Pass the state down 向下传递状态
现在我们已经探索了如何使用 compose 和 ViewModels 来构建单向数据流,让我们来探索 compose 如何在内部与状态交互。
有状态的可组合是一种可以随时间改变的状态组合。
重新组合是再次运行相同的组合以在其数据发生变化时更新树的过程
Compose 生成一棵树,但它与您可能熟悉的 Android 视图系统中的 UI 树有点不同。 compose 生成了一棵可组合的树,而不是一棵 UI 小部件树。
我们不希望每次 重新组合时都会改变。 为此,我们需要一个地方来记住我们在上一个构图中使用的元素。 Compose 允许我们将值存储在组合树中,因此我们可以更新相应的操作 以将【值或者状态】 存储在组合树中。
每次 重构时一些元素都会更新的原因是 有一个隐藏的副作用。 副作用是在可组合函数的执行之外可见的任何更改。
Remember
remember 给出了一个可组合的函数内存。
由记住计算的值将存储在组合树中,并且只有在要记住的键发生变化时才会重新计算。
您可以将 memory 视为将单个对象的存储空间分配给函数,就像私有 val 属性在对象中所做的那样。
可组合函数可以使用 remember
可组合项记住单个对象。系统会在初始组合期间将由 remember
计算的值存储在组合中,并在重组期间返回存储的值。remember
既可用于存储可变对象,又可用于存储不可变对象。