Jetpack Compose 从入门到精通(三):状态管理 - 从原理到实践

180 阅读13分钟

深入理解 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 需要:

  1. 知道 count 的新值是 1
  2. 知道哪些 Composable 读取了 count
  3. 只更新那些 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 的三大作用

  1. 记忆状态:保存 remembermutableStateOf 等的状态,重组时不丢失
  2. 追踪结构:记录 Composable 树的结构,知道哪个组件在哪里
  3. 支持重组:通过对比新旧 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 的场景

  1. 表单数据:用户填写的表单,旋转屏幕后需要保留
  2. 滚动位置:列表的滚动位置
  3. 用户偏好:当前选中的选项、开关状态
  4. 临时数据:未提交的编辑内容

不适合使用 Saver 的场景

  1. 大量数据:Bundle 有大小限制(约 1MB)
  2. 敏感数据:Bundle 数据可以被读取
  3. 不需要持久化的数据:如网络请求结果

三、状态提升(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

特性StateFlowLiveData
生命周期感知需要配合 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

特性derivedStateOfremember
触发条件读取时依赖变化时
适用场景频繁读取、计算成本高一次性计算

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