深入理解 Compose 的状态系统:Slot Table 机制、重组原理、状态提升设计模式,以及企业级状态管理最佳实践。
前言
在前两篇中,我们掌握了 Compose 的布局系统。但一个完整的应用离不开状态管理:用户的点击、网络请求的结果、页面的切换...这些都需要状态来驱动 UI 更新。
Compose 的状态系统与传统 View 系统完全不同,它采用了响应式编程的思想:状态变化自动触发 UI 更新。这听起来很神奇,但背后有着精妙的设计。
本篇文章将带你深入理解:
- Compose 如何追踪状态变化?
- 重组(Recomposition)到底是什么?
- 为什么有时候状态更新了 UI 却没刷新?
- 如何设计优雅的状态管理架构?
让我们开始这场深入源码的探索之旅!
一、状态与重组机制深度解析
1.1 响应式编程的本质
Compose 的核心理念是状态驱动 UI。当状态变化时,UI 会自动更新。这背后的关键机制就是重组(Recomposition)。
传统 View 系统(命令式):
// 手动更新 UI
button.setOnClickListener {
count++
textView.text = "Count: $count" // 手动设置新值
}
Compose(声明式):
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count") // 自动更新
}
}
关键区别:Compose 会自动追踪 count 的变化,当 count 改变时,自动重新执行 Counter 函数,更新 UI。
1.2 Slot Table:Compose 的"记忆"系统
Compose 如何知道哪些 Composable 需要重组?答案就是 Slot Table。
1.2.1 为什么需要 Slot Table?
想象这样一个场景:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
当用户点击按钮,count 从 0 变成 1。此时 Compose 需要:
- 知道
count的新值是 1 - 知道哪些 Composable 读取了
count - 只更新那些 Composable
Slot Table 就是解决这个问题的核心机制。
1.2.2 Slot Table 是什么?
Slot Table 是 Compose 运行时维护的一个线性数组,用于存储 Composable 函数的执行结果。你可以把它理解为 Compose 的"记忆系统"。
┌─────────────────────────────────────────────────────────────────┐
│ Slot Table 结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 索引 类型 内容 │
│ ───────────────────────────────────────────────────────────── │
│ 0 Group Counter (Composable 组) │
│ 1 Int 0 (remember 的 key) │
│ 2 State MutableState(value=0) │
│ 3 Group Button │
│ 4 Lambda onClick = { count++ } │
│ 5 Group Text │
│ 6 String "Count: 0" │
│ 7 EndGroup Text 结束 │
│ 8 EndGroup Button 结束 │
│ 9 EndGroup Counter 结束 │
│ │
└─────────────────────────────────────────────────────────────────┘
Slot Table 的三大作用:
- 记忆状态:保存
remember、mutableStateOf等的状态,重组时不丢失 - 追踪结构:记录 Composable 树的结构,知道哪个组件在哪里
- 支持重组:通过对比新旧 Slot Table,确定哪些部分需要更新
1.2.3 Slot Table 的源码实现
// SlotTable 简化版源码(位于 androidx.compose.runtime 包)
internal class SlotTable {
// 存储数据的数组,所有状态和对象都存在这里
val slots = Array<Any?>(64) { null }
// 当前写入位置
var currentSlot = 0
// 组(Group)的栈,用于追踪嵌套的 Composable
val groups = IntArray(64)
var currentGroup = 0
// 写入数据到当前位置,然后移动到下一个
fun write(value: Any?) {
slots[currentSlot++] = value
}
// 读取当前位置的数据,然后移动到下一个
fun read(): Any? = slots[currentSlot++]
// 开始一个组(Composable)
fun startGroup(key: Int) {
groups[currentGroup++] = currentSlot
write(key)
}
// 结束一个组
fun endGroup() {
currentGroup--
}
}
1.2.4 Slot Table 如何工作?
让我们通过一个完整例子理解:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
首次组合时:
1. 开始 Counter 组
SlotTable.startGroup("Counter".hashCode())
2. 执行 remember { mutableStateOf(0) }
- 创建 MutableState 对象
- 写入 SlotTable: write(MutableState(0))
3. 开始 Button 组
SlotTable.startGroup("Button".hashCode())
4. 执行 Text("Count: $count")
- 读取 count 的值: 0
- 写入 SlotTable: write("Count: 0")
5. 结束所有组
重组时(用户点击后,count 变成 1):
1. 开始 Counter 组
- 发现这个组已经存在
- 跳过已存在的内容,只对比变化
2. 执行 remember
- 发现 remember 已经存在
- 直接返回缓存的 MutableState
3. 执行 Text("Count: $count")
- 发现之前的值是 "Count: 0"
- 新的值是 "Count: 1"
- 值不同!需要更新
4. 只更新 Text 组件
1.3 Composition 的生命周期
Compose 的渲染过程分为三个阶段:
┌─────────────────────────────────────────────────────────────────┐
│ Composition 生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 初始组合 (Initial Composition) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 执行 Composable 函数 │ │
│ │ 2. 填充 Slot Table │ │
│ │ 3. 创建 LayoutNode 树 │ │
│ │ 4. 测量、布局、绘制 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 状态变化 │
│ ↓ │
│ 重组 (Recomposition) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 检测到 State 变化 │ │
│ │ 2. 标记受影响的 Composable 为"过期" │ │
│ │ 3. 重新执行过期的 Composable │ │
│ │ 4. 对比新旧 Slot Table │ │
│ │ 5. 只更新变化的部分 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 布局与绘制 (Layout & Draw) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 测量受影响的节点 │ │
│ │ 2. 布局受影响的节点 │ │
│ │ 3. 绘制受影响的区域 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
1.4 重组的触发机制
1.4.1 State 变化如何触发重组?
// mutableStateOf 的简化实现
fun <T> mutableStateOf(value: T): MutableState<T> =
SnapshotMutableStateImpl(value)
// SnapshotMutableStateImpl 简化版
private class SnapshotMutableStateImpl<T>(initial: T) : MutableState<T> {
@Suppress("UNCHECKED_CAST")
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (it.value != value) {
// 1. 创建新的 State 记录
next = it.newStateRecord(value)
// 2. 通知观察者
notifyObservers()
}
}
}
触发流程:
1. 用户点击按钮
↓
2. count++ 执行
↓
3. MutableState.value = newValue
↓
4. 创建新的 Snapshot 记录
↓
5. 通知所有读取过该 State 的 Composable
↓
6. 将这些 Composable 标记为"需要重组"
↓
7. 在下一帧,执行重组
1.4.2 重组的范围控制
Compose 会尽量缩小重组的范围:
@Composable
fun Parent() {
var count by remember { mutableStateOf(0) }
Column {
// 这个 Text 会重组,因为它读取了 count
Text("Count: $count")
// 这个 Button 不会重组,因为它没有读取 count
Button(onClick = { count++ }) {
Text("Increment")
}
// Child 会重组吗?取决于它是否读取了 count
Child(count)
}
}
@Composable
fun Child(count: Int) {
// 如果这里使用了 count,就会重组
Text("Child: $count")
}
重组范围规则:
- 只有读取了变化 State 的 Composable 才会重组
- 重组会向下传播到所有子 Composable
- 可以通过参数传递控制重组范围
1.5 重组的优化策略
1.5.1 @Stable 与 @Immutable
Compose 使用 稳定性(Stability) 来判断对象变化时是否需要重组:
// 不稳定类 - 任何变化都会触发重组
class UnstableUser {
var name: String = ""
var age: Int = 0
}
// 稳定类 - 只有属性变化时触发重组
@Stable
class StableUser {
var name: String = ""
var age: Int = 0
}
// 不可变类 - 实例替换时触发重组
@Immutable
data class ImmutableUser(
val name: String,
val age: Int
)
稳定性对比:
| 注解 | 说明 | 触发重组条件 |
|---|---|---|
| 无 | 不稳定 | 任何情况都可能重组 |
| @Stable | 稳定 | 属性变化时重组 |
| @Immutable | 不可变 | 实例替换时重组 |
1.5.2 remember 的妙用
@Composable
fun ExpensiveCalculation(data: List<Int>) {
// ❌ 错误:每次重组都重新计算
val sum = data.sum()
// ✅ 正确:只在 data 变化时计算
val sum = remember(data) { data.sum() }
Text("Sum: $sum")
}
1.5.3 derivedStateOf:派生状态
当状态变化频繁,但 UI 不需要每次都更新时,使用 derivedStateOf:
@Composable
fun SearchList(query: String, items: List<Item>) {
// ❌ 错误:每次 query 变化都过滤整个列表
val filtered = items.filter { it.name.contains(query) }
// ✅ 正确:只在必要时更新
val filtered by remember {
derivedStateOf {
items.filter { it.name.contains(query) }
}
}
LazyColumn {
items(filtered) { item ->
ItemRow(item)
}
}
}
derivedStateOf 的原理:
- 内部维护一个缓存值
- 只有读取时才会重新计算
- 计算结果变化时才通知观察者
二、remember 与 mutableStateOf 源码解析
2.1 remember 的原理
remember 是 Compose 中用于缓存计算结果的函数。它的核心作用是:在重组时保持状态不变。
2.1.1 为什么需要 remember?
看一个例子:
@Composable
fun Counter() {
var count = 0 // ❌ 错误:每次重组都会重置为 0
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
每次用户点击按钮,触发重组,count 都会被重新初始化为 0。这是因为我们没有"记住"这个值。
remember 的作用:在首次组合时计算值,并在后续重组时返回缓存的值。
2.1.2 remember 的源码实现
// remember 的简化实现
@Composable
inline fun <T> remember(
vararg keys: Any?, // 缓存的 key
calculation: () -> T // 计算函数
): T {
// 获取当前 Composer
val composer = currentComposer
// 在 Slot Table 中查找缓存
val slot = composer.nextSlot()
return if (slot === Composer.Empty ||
keys.any { it != composer.rememberedValue() }) {
// 缓存不存在或 key 变化,重新计算
val value = calculation()
composer.updateRememberedValue(value)
value
} else {
// 使用缓存
@Suppress("UNCHECKED_CAST")
slot as T
}
}
执行流程:
┌─────────────────────────────────────────────────────────────────┐
│ remember 执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 第一次组合 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 读取 Slot Table → 得到 Composer.Empty │ │
│ │ 2. 执行 calculation() │ │
│ │ 3. 将结果写入 Slot Table │ │
│ │ 4. 返回结果 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 重组(key 未变化) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 读取 Slot Table → 得到缓存值 │ │
│ │ 2. 对比 keys → 没有变化 │ │
│ │ 3. 直接返回缓存值(跳过了 calculation) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 重组(key 变化) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 读取 Slot Table → 得到缓存值 │ │
│ │ 2. 对比 keys → 有变化 │ │
│ │ 3. 执行 calculation() │ │
│ │ 4. 更新 Slot Table │ │
│ │ 5. 返回新结果 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.1.3 remember 的多种用法
// 1. 无 key - 永远使用缓存
val value = remember { expensiveCalculation() }
// 2. 有 key - key 变化时重新计算
val value = remember(userId) { loadUser(userId) }
// 3. 多个 key - 任一 key 变化都重新计算
val value = remember(userId, refreshFlag) { loadUser(userId) }
// 4. rememberSaveable - 配置变更后恢复
val value = rememberSaveable { mutableStateOf(0) }
2.2 mutableStateOf 的原理
mutableStateOf 创建了一个可观察的状态对象,当值变化时会自动触发重组。
2.2.1 mutableStateOf 的源码实现
// mutableStateOf 实现
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
// SnapshotMutableStateImpl 简化版
private class SnapshotMutableStateImpl<T>(
initial: T,
private val policy: SnapshotMutationPolicy<T>
) : MutableState<T> {
@Suppress("UNCHECKED_CAST")
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
// 使用 policy 判断是否相等
if (!policy.equivalent(it.value, value)) {
// 创建新的 State 记录
next = it.newStateRecord(value)
}
}
override fun component1(): T = value
override fun component2(): (T) -> Unit = { value = it }
}
2.2.2 Snapshot 机制
Compose 的 State 基于 Snapshot(快照) 机制实现:
┌─────────────────────────────────────────────────────────────────┐
│ Snapshot 机制 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Snapshot 1 (当前) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ StateRecord 1: value = "A" │ │
│ │ StateRecord 2: value = "B" ← 当前值 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ value = "C" (修改) │
│ ↓ │
│ Snapshot 2 (新快照) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ StateRecord 1: value = "A" │ │
│ │ StateRecord 2: value = "B" │ │
│ │ StateRecord 3: value = "C" ← 新记录 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 通知观察者:值从 "B" 变为 "C" │
│ │
└─────────────────────────────────────────────────────────────────┘
Snapshot 的优势:
- 原子性:多个 State 的修改要么全部生效,要么全部不生效
- 一致性:读取时看到的是一致的快照
- 可回滚:可以丢弃未提交的修改
2.2.3 状态比较策略
mutableStateOf 支持三种比较策略:
// 1. 结构相等(默认)- 使用 == 比较
val state = mutableStateOf(listOf(1, 2, 3))
state.value = listOf(1, 2, 3) // 不会触发重组,因为内容相等
// 2. 引用相等 - 使用 === 比较
val state = mutableStateOf(listOf(1, 2, 3), policy = referentialEqualityPolicy())
state.value = listOf(1, 2, 3) // 会触发重组,因为是新对象
// 3. 永不比较 - 总是认为不同
val state = mutableStateOf(0, policy = neverEqualPolicy())
state.value = 0 // 会触发重组
2.3 rememberSaveable 的原理
rememberSaveable 可以在配置变更(如屏幕旋转)后恢复状态。
2.3.1 为什么需要 rememberSaveable?
看一个场景:
@Composable
fun FormScreen() {
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
// 用户填写了表单...
// 旋转屏幕后,name 和 email 都变成空字符串了!
// 因为 Activity 重建,remember 的缓存丢失了
}
rememberSaveable 的作用:将状态保存到 Bundle,配置变更后自动恢复。
2.3.2 实现原理
@Composable
inline fun <T : Any> rememberSaveable(
vararg inputs: Any?,
saver: Saver<T, out Any> = autoSaver(),
key: String? = null,
init: () -> T
): T {
// 组合本地提供的 SaveableStateRegistry
val registry = LocalSaveableStateRegistry.current
// 生成唯一 key
val finalKey = key ?: rememberSaveableKey()
// 尝试从 Bundle 恢复
val restored = registry?.consumeRestored(finalKey)
// 创建或恢复状态
val value = remember(*inputs) {
restored?.let { saver.restore(it) } ?: init()
}
// 注册保存回调
if (registry != null) {
DisposableEffect(registry, finalKey, saver) {
val entry = registry.registerProvider(finalKey) {
saver.save(value)
}
onDispose { entry.unregister() }
}
}
return value
}
保存与恢复流程:
┌─────────────────────────────────────────────────────────────────┐
│ rememberSaveable 流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 首次创建 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 检查 Bundle 中没有该 key 的保存值 │ │
│ │ 2. 执行 init() 创建初始值 │ │
│ │ 3. 注册保存回调到 SaveableStateRegistry │ │
│ │ 4. 返回初始值 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 配置变更(如旋转屏幕) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Activity 被销毁,触发 onSaveInstanceState │ │
│ │ 2. SaveableStateRegistry 调用所有注册的保存回调 │ │
│ │ 3. 将值序列化保存到 Bundle │ │
│ │ 4. Activity 重建 │ │
│ │ 5. 从 Bundle 恢复值 │ │
│ │ 6. rememberSaveable 读取恢复的值 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3.3 自定义 Saver
什么是 Saver?
Saver 是 rememberSaveable 用来序列化和反序列化状态的工具。因为 Bundle 只能存储特定类型的数据,我们需要告诉 Compose 如何将自定义对象转换为 Bundle 可存储的格式。
为什么需要自定义 Saver?
// 假设我们有一个自定义类
data class User(
val id: Int,
val name: String,
val email: String
)
// ❌ 错误:直接 rememberSaveable 会崩溃
var user by rememberSaveable { mutableStateOf(User(1, "张三", "zhangsan@example.com")) }
// 崩溃原因:User 类不知道如何保存到 Bundle
自定义 Saver 示例:
// 为自定义类创建 Saver
class User(val id: Int, val name: String, val email: String)
val UserSaver = Saver<User, Bundle>(
// save: 将 User 转换为 Bundle
save = { user ->
Bundle().apply {
putInt("id", user.id)
putString("name", user.name)
putString("email", user.email)
}
},
// restore: 从 Bundle 恢复 User
restore = { bundle ->
User(
id = bundle.getInt("id"),
name = bundle.getString("name") ?: "",
email = bundle.getString("email") ?: ""
)
}
)
// 使用自定义 Saver
@Composable
fun UserScreen() {
var user by rememberSaveable(saver = UserSaver) {
mutableStateOf(User(1, "张三", "zhangsan@example.com"))
}
// 现在旋转屏幕后,user 的值会被正确恢复
Text("Name: ${user.name}")
}
Saver 生效机制:
┌─────────────────────────────────────────────────────────────────┐
│ Saver 生效流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 保存时 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. rememberSaveable 检测到配置变更 │ │
│ │ 2. 调用 Saver.save(value) │ │
│ │ 3. 返回 Bundle/Parcelable/List 等可序列化类型 │ │
│ │ 4. 系统自动保存到 Activity 的 Bundle │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 恢复时 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Activity 重建,从 Bundle 读取保存的数据 │ │
│ │ 2. rememberSaveable 调用 Saver.restore(savedData) │ │
│ │ 3. 返回恢复后的对象 │ │
│ │ 4. 赋值给状态变量 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
适合使用自定义 Saver 的场景:
- 表单数据:用户填写的表单,旋转屏幕后需要保留
- 滚动位置:列表的滚动位置
- 用户偏好:当前选中的选项、开关状态
- 临时数据:未提交的编辑内容
不适合使用 Saver 的场景:
- 大量数据:Bundle 有大小限制(约 1MB)
- 敏感数据:Bundle 数据可以被读取
- 不需要持久化的数据:如网络请求结果
三、状态提升(State Hoisting)设计模式
3.1 什么是状态提升?
状态提升是 Compose 推荐的状态管理模式:将状态从子组件"提升"到父组件,子组件通过参数接收状态,通过回调通知状态变化。
为什么叫"提升"?
想象一个组件树:
Parent
├── ChildA
│ └── GrandChild
└── ChildB
如果 GrandChild 需要修改状态,而这个状态需要影响 ChildB,那么状态就需要"提升"到它们的共同祖先 Parent。
3.2 状态提升的设计原则
单一数据源:状态只在一个地方定义 状态向下流动:通过参数传递给子组件 事件向上传递:通过回调通知父组件修改状态
3.3 状态提升的示例
3.3.1 无状态提升(问题代码)
// ❌ 错误:状态分散在各个组件中
@Composable
fun CounterScreen() {
Column {
Counter() // 每个 Counter 有自己的状态
Counter()
Counter()
}
}
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) } // 状态在子组件中
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
问题:
- 无法同步多个 Counter 的状态
- 父组件无法获取/修改子组件的状态
- 状态分散,难以管理
3.3.2 有状态提升(正确代码)
// ✅ 正确:状态提升到父组件
@Composable
fun CounterScreen() {
var counts by remember { mutableStateOf(List(3) { 0 }) }
Column {
counts.forEachIndexed { index, count ->
Counter(
count = count,
onIncrement = {
counts = counts.toMutableList().apply {
this[index]++
}
}
)
}
}
}
@Composable
fun Counter(
count: Int, // 状态通过参数传入
onIncrement: () -> Unit // 事件通过回调传出
) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}
优势:
- 状态集中管理
- 子组件可复用
- 易于测试
3.4 状态提升的设计模式
3.4.1 状态容器模式
对于复杂的状态,可以创建专门的状态容器类:
// 状态容器
class CounterState {
var count by mutableStateOf(0)
private set
fun increment() {
count++
}
fun decrement() {
count--
}
}
// 使用 remember 创建状态容器
@Composable
fun rememberCounterState() = remember { CounterState() }
// 使用
@Composable
fun CounterScreen() {
val state = rememberCounterState()
Counter(
count = state.count,
onIncrement = { state.increment() }
)
}
3.4.2 屏幕级状态管理
对于整个屏幕的状态,可以在屏幕级 Composable 中统一管理:
@Composable
fun UserProfileScreen(
userId: String,
viewModel: UserProfileViewModel = hiltViewModel()
) {
// 收集 ViewModel 的状态
val uiState by viewModel.uiState.collectAsState()
// 屏幕级状态管理
var isEditing by remember { mutableStateOf(false) }
var editedName by remember { mutableStateOf("") }
when (val state = uiState) {
is UiState.Loading -> LoadingScreen()
is UiState.Error -> ErrorScreen(state.message)
is UiState.Success -> {
UserProfileContent(
user = state.data,
isEditing = isEditing,
editedName = editedName,
onEditClick = {
isEditing = true
editedName = state.data.name
},
onNameChange = { editedName = it },
onSaveClick = {
viewModel.updateName(editedName)
isEditing = false
}
)
}
}
}
3.5 状态提升的边界
不是所有状态都需要提升,需要根据场景判断:
| 状态位置 | 适用场景 |
|---|---|
| 子组件内部 | 纯 UI 状态(如动画状态、滚动位置) |
| 父组件 | 需要共享的状态、需要持久化的状态 |
| ViewModel | 业务逻辑状态、需要跨屏幕共享的状态 |
四、ViewModel 与 Compose 最佳实践
4.1 ViewModel 的作用
ViewModel 是 Android 架构组件,用于:
- 保存配置变更后的状态
- 管理业务逻辑
- 作为 UI 层和数据层的桥梁
4.2 ViewModel 与 Compose 的集成
// ViewModel 定义
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
// 使用 StateFlow 管理状态
private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
init {
loadUser()
}
private fun loadUser() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = userRepository.getUser()
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
fun refresh() {
loadUser()
}
}
// Composable 中使用
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
when (val state = uiState) {
is UiState.Loading -> LoadingScreen()
is UiState.Error -> ErrorScreen(
message = state.message,
onRetry = { viewModel.refresh() }
)
is UiState.Success -> UserContent(user = state.data)
}
}
4.3 StateFlow vs LiveData
在 Compose 中,推荐使用 StateFlow:
| 特性 | StateFlow | LiveData |
|---|---|---|
| 生命周期感知 | 需要配合 collectAsState | 自动感知 |
| 初始值 | 必须有 | 可以没有 |
| 线程安全 | 是 | 是 |
| 与 Compose 集成 | 更自然 | 需要额外转换 |
4.4 UI 状态设计最佳实践
4.4.1 单一状态类
// ✅ 推荐:使用单一状态类
sealed interface UserUiState {
data object Loading : UserUiState
data class Success(val user: User) : UserUiState
data class Error(val message: String) : UserUiState
}
// ❌ 不推荐:分散的状态
class UserViewModel : ViewModel() {
val isLoading = MutableStateFlow(false)
val user = MutableStateFlow<User?>(null)
val error = MutableStateFlow<String?>(null)
}
4.4.2 不可变状态
// ✅ 推荐:状态不可变,通过 copy 更新
data class UserUiState(
val isLoading: Boolean = false,
val user: User? = null,
val error: String? = null
)
// ViewModel 中
private val _uiState = MutableStateFlow(UserUiState())
val uiState = _uiState.asStateFlow()
fun updateUser(user: User) {
_uiState.update { it.copy(user = user, isLoading = false) }
}
4.5 ViewModel 的生命周期
┌─────────────────────────────────────────────────────────────────┐
│ ViewModel 生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Activity/Fragment 创建 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. ViewModel 创建 │ │
│ │ 2. 执行 init 块 │ │
│ │ 3. 开始收集数据 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 配置变更(旋转屏幕) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Activity 销毁 │ │
│ │ 2. ViewModel 保留(不销毁) │ │
│ │ 3. 新的 Activity 创建 │ │
│ │ 4. 获取相同的 ViewModel 实例 │ │
│ │ 5. 状态保持不变 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ Activity/Fragment 真正销毁 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. onCleared() 被调用 │ │
│ │ 2. ViewModel 销毁 │ │
│ │ 3. 协程作用域取消 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
五、CompositionLocal 深度解析
5.1 什么是 CompositionLocal?
CompositionLocal 是一种隐式传参机制,允许在 Composable 树中向下传递数据,而不需要通过每一层的参数。
为什么需要 CompositionLocal?
想象这样一个场景:
// 不使用 CompositionLocal - 需要层层传递
@Composable
fun App() {
val user = User("张三", "admin")
ScreenA(user = user)
}
@Composable
fun ScreenA(user: User) {
Header(user = user)
Content(user = user)
}
@Composable
fun Header(user: User) {
Avatar(user = user)
UserName(user = user)
}
// 每一层都需要传递 user 参数,即使中间层不使用它
使用 CompositionLocal:
// 使用 CompositionLocal - 隐式传递
val LocalUser = compositionLocalOf<User> { error("No user provided") }
@Composable
fun App() {
val user = User("张三", "admin")
CompositionLocalProvider(LocalUser provides user) {
ScreenA() // 不需要传递 user
}
}
@Composable
fun ScreenA() {
Header() // 不需要传递 user
Content() // 不需要传递 user
}
@Composable
fun Header() {
val user = LocalUser.current // 在需要的地方获取
Avatar(user = user)
UserName(user = user)
}
5.2 CompositionLocal 的原理
┌─────────────────────────────────────────────────────────────────┐
│ CompositionLocal 原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Composable 树 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ CompositionLocalProvider(LocalColor provides Color.Red) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Child1() │ │ │
│ │ │ ┌─────────────────────────────────────────────┐ │ │ │
│ │ │ │ Child2() │ │ │ │
│ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ LocalColor.current // 返回 Red │ │ │ │ │
│ │ │ │ └─────────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 查找规则: │
│ 1. 从当前节点向上查找 │
│ 2. 找到最近的 CompositionLocalProvider │
│ 3. 返回对应的值 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 创建与使用 CompositionLocal
// 1. 创建 CompositionLocal
val LocalUser = compositionLocalOf<User> { error("No user provided") }
// 2. 提供值
CompositionLocalProvider(LocalUser provides currentUser) {
UserProfile() // 及其子组件都可以通过 LocalUser.current 获取
}
// 3. 使用值
@Composable
fun UserProfile() {
val user = LocalUser.current
Text("Hello, ${user.name}")
}
5.4 compositionLocalOf vs staticCompositionLocalOf
| 函数 | 特点 | 适用场景 |
|---|---|---|
compositionLocalOf | 值变化时,只重组读取该值的组件 | 频繁变化的值 |
staticCompositionLocalOf | 值变化时,重组整个子树 | 很少变化的值 |
// 主题颜色 - 很少变化
val LocalColors = staticCompositionLocalOf { LightColorScheme }
// 滚动状态 - 频繁变化
val LocalScrollState = compositionLocalOf { ScrollState(0) }
5.5 MaterialTheme 的实现
MaterialTheme 就是基于 CompositionLocal 实现的:
@Composable
fun MaterialTheme(
colorScheme: ColorScheme = MaterialTheme.colorScheme,
typography: Typography = MaterialTheme.typography,
shapes: Shapes = MaterialTheme.shapes,
content: @Composable () -> Unit
) {
val rememberedColorScheme = remember { colorScheme }
val rememberedTypography = remember { typography }
val rememberedShapes = remember { shapes }
CompositionLocalProvider(
LocalColorScheme provides rememberedColorScheme,
LocalTypography provides rememberedTypography,
LocalShapes provides rememberedShapes
) {
content()
}
}
// 使用
@Composable
fun MyComponent() {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
Text(
text = "Hello",
color = colorScheme.primary,
style = typography.bodyLarge
)
}
5.6 CompositionLocal 的最佳实践
✅ 推荐使用
- 主题配置(颜色、字体、形状)
- 上下文信息(屏幕类型、权限状态)
- 全局服务(Navigator、Analytics)
❌ 避免使用
- 频繁变化的数据(使用参数传递)
- 业务逻辑状态(使用 ViewModel)
- 可能导致隐式依赖,降低代码可读性
六、副作用 API 详解
6.1 什么是副作用?
副作用是指 Composable 函数对外部世界的影响,如:
- 网络请求
- 数据库操作
- 订阅事件
- 修改共享状态
Compose 是纯函数式的,Composable 不应该有副作用。但应用开发离不开副作用,因此 Compose 提供了专门的 API 来处理副作用。
6.2 LaunchedEffect:在组合中启动协程
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
// 在组合时启动协程
LaunchedEffect(userId) {
// 这里的代码在协程中执行
user = api.getUser(userId)
}
user?.let {
Text("Name: ${it.name}")
}
}
LaunchedEffect 的特点:
- 在组合时启动协程
- key 变化时,取消旧协程,启动新协程
- 重组时不会重新启动
┌─────────────────────────────────────────────────────────────────┐
│ LaunchedEffect 生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 首次组合 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. LaunchedEffect 进入组合 │ │
│ │ 2. 启动协程,执行副作用代码 │ │
│ │ 3. 协程持续运行... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 重组(key 未变化) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. LaunchedEffect 重组 │ │
│ │ 2. key 相同,协程继续运行 │ │
│ │ 3. 不执行副作用代码 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 重组(key 变化) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 取消旧协程 │ │
│ │ 2. 启动新协程 │ │
│ │ 3. 执行新的副作用代码 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 退出组合 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. LaunchedEffect 退出组合 │ │
│ │ 2. 取消协程 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.3 SideEffect:每次重组都执行
@Composable
fun AnalyticsScreen(screenName: String) {
// 每次重组都执行
SideEffect {
analytics.logScreenView(screenName)
}
// UI 内容
Text("Screen: $screenName")
}
使用场景:需要确保副作用在每次成功重组后都执行。
6.4 DisposableEffect:需要清理的副作用
@Composable
fun BackHandler(onBack: () -> Unit) {
val dispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
DisposableEffect(dispatcher) {
// 设置回调
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBack()
}
}
dispatcher?.addCallback(callback)
// 返回清理函数
onDispose {
callback.remove()
}
}
}
DisposableEffect 的特点:
- key 变化时,先执行 onDispose,再执行新的副作用
- 退出组合时,执行 onDispose
6.5 rememberCoroutineScope:在回调中启动协程
@Composable
fun UserList() {
val scope = rememberCoroutineScope()
var users by remember { mutableStateOf<List<User>>(emptyList()) }
Button(onClick = {
// 在回调中启动协程
scope.launch {
users = api.getUsers()
}
}) {
Text("Load Users")
}
// 显示用户列表
LazyColumn {
items(users) { user ->
Text(user.name)
}
}
}
与 LaunchedEffect 的区别:
LaunchedEffect:在组合时自动启动rememberCoroutineScope:提供 CoroutineScope,在回调中手动启动
6.6 rememberUpdatedState:获取最新值
当需要在长时间运行的副作用中获取最新状态时,使用 rememberUpdatedState:
@Composable
fun Timer(
onTick: () -> Unit,
interval: Long = 1000L
) {
// 使用 rememberUpdatedState 获取最新的 onTick
val currentOnTick by rememberUpdatedState(onTick)
LaunchedEffect(Unit) {
while (true) {
delay(interval)
currentOnTick() // 总是调用最新的 onTick
}
}
}
为什么需要 rememberUpdatedState?
// ❌ 错误:LaunchedEffect 捕获的是旧的 onTick
LaunchedEffect(Unit) {
while (true) {
delay(1000)
onTick() // 总是调用第一次传入的 onTick
}
}
// ✅ 正确:使用 rememberUpdatedState
val currentOnTick by rememberUpdatedState(onTick)
LaunchedEffect(Unit) {
while (true) {
delay(1000)
currentOnTick() // 总是调用最新的 onTick
}
}
6.7 DerivedState:派生状态
当需要基于其他状态计算派生状态时,使用 derivedStateOf:
@Composable
fun SearchResults(query: String, items: List<Item>) {
// 只在必要时重新计算
val filteredItems by remember {
derivedStateOf {
items.filter { it.name.contains(query, ignoreCase = true) }
}
}
LazyColumn {
items(filteredItems) { item ->
ItemRow(item)
}
}
}
derivedStateOf vs remember:
| 特性 | derivedStateOf | remember |
|---|---|---|
| 触发条件 | 读取时 | 依赖变化时 |
| 适用场景 | 频繁读取、计算成本高 | 一次性计算 |
6.8 produceState:将非 Compose 状态转换为 State
@Composable
fun loadUser(userId: String): State<User?> {
return produceState<User?>(initialValue = null, userId) {
value = api.getUser(userId)
}
}
// 使用
@Composable
fun UserProfile(userId: String) {
val user by loadUser(userId)
user?.let {
Text("Name: ${it.name}")
}
}
6.9 snapshotFlow:将 State 转换为 Flow
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
// 将 State 转换为 Flow
val searchResults by remember {
snapshotFlow { query }
.debounce(300) // 防抖
.flatMapLatest { api.search(it) }
.collectAsState(initial = emptyList())
}
Column {
TextField(
value = query,
onValueChange = { query = it }
)
LazyColumn {
items(searchResults) { result ->
Text(result.name)
}
}
}
}
七、状态管理架构设计
7.1 分层架构
┌─────────────────────────────────────────────────────────────────┐
│ UI Layer (Composable) │
│ - 显示数据 │
│ - 处理用户交互 │
│ - 调用 ViewModel 方法 │
├─────────────────────────────────────────────────────────────────┤
│ ViewModel Layer │
│ - 管理 UI 状态 │
│ - 处理业务逻辑 │
│ - 调用 Repository 方法 │
├─────────────────────────────────────────────────────────────────┤
│ Repository Layer │
│ - 数据聚合 │
│ - 业务规则 │
│ - 选择数据源 │
├─────────────────────────────────────────────────────────────────┤
│ Data Source Layer │
│ - 网络 API (Retrofit) │
│ - 本地数据库 (Room) │
│ - 本地缓存 (DataStore) │
└─────────────────────────────────────────────────────────────────┘
7.2 完整实战:用户列表页面
// ============ Data Layer ============
// 数据模型
data class User(
val id: String,
val name: String,
val email: String,
val avatar: String
)
// Repository
interface UserRepository {
suspend fun getUsers(): List<User>
suspend fun searchUsers(query: String): List<User>
}
class UserRepositoryImpl @Inject constructor(
private val api: UserApi
) : UserRepository {
override suspend fun getUsers(): List<User> = api.getUsers()
override suspend fun searchUsers(query: String): List<User> = api.searchUsers(query)
}
// ============ ViewModel Layer ============
sealed interface UsersUiState {
data object Loading : UsersUiState
data class Success(val users: List<User>) : UsersUiState
data class Error(val message: String) : UsersUiState
}
@HiltViewModel
class UsersViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UsersUiState>(UsersUiState.Loading)
val uiState: StateFlow<UsersUiState> = _uiState.asStateFlow()
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
// 搜索结果的派生状态
val searchResults: StateFlow<UsersUiState> = _searchQuery
.debounce(300)
.flatMapLatest { query ->
if (query.isEmpty()) {
flowOf(UsersUiState.Success(emptyList()))
} else {
flow {
emit(UsersUiState.Loading)
try {
val users = repository.searchUsers(query)
emit(UsersUiState.Success(users))
} catch (e: Exception) {
emit(UsersUiState.Error(e.message ?: "Unknown error"))
}
}
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UsersUiState.Success(emptyList())
)
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_uiState.value = UsersUiState.Loading
try {
val users = repository.getUsers()
_uiState.value = UsersUiState.Success(users)
} catch (e: Exception) {
_uiState.value = UsersUiState.Error(e.message ?: "Unknown error")
}
}
}
fun onSearchQueryChange(query: String) {
_searchQuery.value = query
}
fun refresh() {
loadUsers()
}
}
// ============ UI Layer ============
@Composable
fun UsersScreen(
viewModel: UsersViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val searchQuery by viewModel.searchQuery.collectAsState()
val searchResults by viewModel.searchResults.collectAsState()
val isSearching = searchQuery.isNotEmpty()
val currentState = if (isSearching) searchResults else uiState
UsersContent(
uiState = currentState,
searchQuery = searchQuery,
onSearchQueryChange = viewModel::onSearchQueryChange,
onRefresh = viewModel::refresh
)
}
@Composable
fun UsersContent(
uiState: UsersUiState,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
onRefresh: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
// 搜索框
SearchBar(
query = searchQuery,
onQueryChange = onSearchQueryChange
)
// 内容区域
when (val state = uiState) {
is UsersUiState.Loading -> LoadingScreen()
is UsersUiState.Error -> ErrorScreen(
message = state.message,
onRetry = onRefresh
)
is UsersUiState.Success -> {
if (state.users.isEmpty()) {
EmptyScreen()
} else {
UserList(users = state.users)
}
}
}
}
}
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users, key = { it.id }) { user ->
UserItem(user = user)
}
}
}
@Composable
fun UserItem(user: User) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = user.avatar,
contentDescription = null,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = user.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
}
}
}
}
八、本篇小结
今天我们深入探讨了 Compose 的状态管理系统:
状态与重组机制
- 理解了 Slot Table 的数据结构和作用
- 掌握了重组的触发机制和优化策略
- 学会了使用 @Stable 和 @Immutable 控制重组
remember 与 mutableStateOf
- 深入理解了 remember 的缓存原理
- 掌握了 mutableStateOf 的 Snapshot 机制
- 学会了 rememberSaveable 的保存与恢复
状态提升
- 理解了状态提升的设计哲学
- 掌握了状态容器模式和屏幕级状态管理
ViewModel 与 Compose
- 学会了 StateFlow 与 Compose 的集成
- 掌握了 UI 状态设计的最佳实践
CompositionLocal
- 理解了隐式传参的原理
- 学会了创建和使用 CompositionLocal
副作用 API
- 掌握了 LaunchedEffect、DisposableEffect 等副作用 API
- 理解了不同场景下的选择策略
下篇预告
第四篇:Material 组件与主题 将深入讲解:
- Material Design 3 组件大全
- 自定义主题与颜色系统
- 深色模式与动态主题适配
- 自定义组件的设计思路
敬请期待!
参考资源
📌 系列文章导航
- 第一篇:初识 Compose ✅
- 第二篇:核心基石 ✅
- 第三篇:状态管理(当前)✅
- 第四篇:Material 组件与主题
- 第五篇:动画与交互
- 第六篇:架构与工程化
- 第七篇:高级特性与实战
如果这篇文章对你有帮助,欢迎 点赞、收藏、关注!有任何问题可以在评论区留言。
Generated by Kimi K2.5 Agent