A
第 48 章KOTLIN40 分钟

Compose 状态管理

深入 Jetpack Compose 状态管理体系:remember 与 rememberSaveable、mutableStateOf 与 by 委托、状态提升、derivedStateOf、CompositionLocal、SavedStateHandle 与单向数据流原则。

学习目标

  • 掌握 remember 与 rememberSaveable 的区别与使用场景
  • 理解 mutableStateOf 与 by 委托的状态读写机制
  • 能够进行状态提升,编写无状态可复用组件
  • 掌握 derivedStateOf 与 CompositionLocal 的使用时机
  • 理解 SavedStateHandle 与配置变更/进程恢复的关系

学习目标

  • 掌握 remember 与 rememberSaveable 的区别与使用场景;
  • 理解 mutableStateOf 与 by 委托的状态读写机制;
  • 能够进行状态提升,编写无状态(stateless)可复用组件;
  • 掌握 derivedStateOf 与 CompositionLocal 的使用时机;
  • 理解 SavedStateHandle 与配置变更/进程恢复的关系,并贯彻单向数据流原则。

Compose 状态管理

Jetpack Compose 是声明式 UI 框架,其核心思想是 UI 是状态的函数:UI = f(state)。当状态变化时,Compose 会自动重新执行(重组,Recomposition)受影响的可组合函数,刷新对应界面。因此,“状态管理”在 Compose 中是比“命令式操作 View”更重要的基础课题。

依赖配置

dependencies {
    // Compose BOM 统一管理版本
    val composeBom = platform("androidx.compose:compose-bom:2024.09.03")
    implementation(composeBom)
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.activity:activity-compose:1.9.2")
    // 生命周期相关
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.6")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
}

使用 BOM 后,所有 androidx.compose.* 工件无需再写版本号,BOM 会保证彼此兼容。

状态的基本概念

在 Compose 中,状态(State) 是任何能驱动 UI 变化的值。最典型的形式是 MutableState<T>,它持有一个可变值,读取它的可组合函数会在值变化时自动重组。

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

// 1. 直接读取 .value
val count = mutableStateOf(0)
// 读取:count.value
// 写入:count.value++

// 2. 使用 by 委托(推荐,代码更简洁)
var count by mutableStateOf(0)
// 读取:count
// 写入:count++

使用 by 委托必须导入 getValue 与 setValue 两个扩展,否则编译器会报错。

remember:在重组间保留状态

可组合函数在状态变化时会被反复重新调用。如果直接在函数体内 val count = mutableStateOf(0),每次重组都会创建一个 新的 MutableState,状态永远归零。remember 的作用是把首次计算结果 存储在 Composition 中,后续重组直接复用:

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

@Composable
fun CounterScreen() {
    // remember 把 mutableStateOf(0) 的结果缓存起来,重组时不会重新创建
    var count by remember { mutableStateOf(0) }
    Column {
        Text("点击次数:$count")
        Button(onClick = { count++ }) {
            Text("加一")
        }
    }
}

remember 接受一个计算块,仅在该 Composable 首次进入组合(first composition)时执行,之后重组直接返回缓存的引用。

remember 的 key 依赖

remember(key1, key2) 在 key 变化时会 丢弃旧值并重新计算,常用于“参数变了状态要重置”的场景:

@Composable
fun UserHeader(userId: String) {
    // userId 改变时,输入框内容会被重置
    var draft by remember(userId) { mutableStateOf("") }
    TextField(value = draft, onValueChange = { draft = it })
}

一个常见坑:把可变对象本身作为 key,但对象内部字段变了而引用没变,key 不会触发重新计算。务必使用值类型或稳定字段作为 key。

rememberSaveable:跨配置变更保留

remember 只能存活于“重组”,但旋转屏幕(配置变更)会销毁整个 Activity,remember 缓存随之丢失。rememberSaveable 会把值写入 SavedStateRegistry(最终走 onSaveInstanceState),配置变更后自动恢复:

import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun LoginScreen() {
    var username by rememberSaveable { mutableStateOf("") }
    var password by rememberSaveable { mutableStateOf("") }
    // 旋转屏幕后,输入框内容仍在
    TextField(value = username, onValueChange = { username = it })
}

rememberSaveable 默认支持基本类型(Int/String/Boolean 等以及它们的 MutableState)。对于自定义对象,必须让它能被保存:

  • 方式一:实现 Parcelable 接口;
  • 方式二:提供自定义 Saver。

Parcelable 方式

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class UserInfo(val id: String, val name: String, val avatar: String) : Parcelable

@Composable
fun ProfileScreen() {
    var user by rememberSaveable { mutableStateOf(UserInfo("1", "Tom", "")) }
    // ...
}

需要在 build.gradle 中启用 id("kotlin-parcelize") 插件。

自定义 Saver

对于无法直接实现 Parcelable 的类型(如框架类、第三方类),可以提供 Saver:

import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable

data class SearchFilter(val keyword: String, val category: Int)

val SearchFilterSaver = Saver<SearchFilter, Pair<String, Int>>(
    save = { it.keyword to it.category },
    restore = { (keyword, category) -> SearchFilter(keyword, category) }
)

@Composable
fun SearchScreen() {
    var filter by rememberSaveable(stateSaver = SearchFilterSaver) {
        mutableStateOf(SearchFilter("", 0))
    }
    // ...
}

rememberSaveable 与 remember 的关键差异:rememberSaveable 走系统 SavedState,因此值必须能序列化;remember 仅在内存中,可放任意对象(如 CoroutineScope、Painter)。

状态提升:从有状态到无状态

状态提升(State Hoisting) 是把状态从组件内部移到调用方的模式。一个无状态(stateless)组件只接收“值”和“回调”,不自己持有状态。它的好处是:可复用、可测试、可预览。

对比下面两个版本:

// 有状态(stateful):内部维护状态,不易复用
@Composable
fun StatefulNameInput() {
    var name by remember { mutableStateOf("") }
    TextField(value = name, onValueChange = { name = it })
}

// 无状态(stateless):状态由调用方持有
@Composable
fun StatelessNameInput(
    name: String,
    onNameChange: (String) -> Unit
) {
    TextField(value = name, onValueChange = onNameChange)
}

调用无状态组件时,调用方决定状态如何存储:

@Composable
fun FormScreen() {
    var name by rememberSaveable { mutableStateOf("") }
    StatelessNameInput(name = name, onNameChange = { name = it })
}

状态提升遵循三个原则:

  1. 状态提升到至少所有使用它的组件的共同父级;
  2. 状态只下传,事件只上传(单向数据流);
  3. 不可变状态向下,可变状态留在最上层。

经验法则:如果一个状态只被本组件使用,就放在组件里;被多个组件共享,就提升到共同父级。复杂场景下共享状态请放到 ViewModel(见下一章)。

derivedStateOf:派生状态

当一个状态是由其他状态计算得来时,用 derivedStateOf 可以避免重组过频。例如“列表是否滚动到顶部”由 scrollOffset 派生,但 scrollOffset 每帧都在变,没必要每次都重组依赖它的组件:

import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue

@Composable
fun TopBarShowcase(scrollProvider: () -> Int) {
    // 只有当 showTopBar 真正翻转(false<->true)时才触发重组
    val showTopBar by remember {
        derivedStateOf { scrollProvider() < 100 }
    }
    AnimatedVisibility(visible = showTopBar) {
        Text("回到顶部")
    }
}

derivedStateOf 会缓存上次结果,只有当派生值 实际改变 时,订阅它的 Composable 才会重组。常用于:

  • 按钮是否可点击(表单是否全部合法);
  • 列表是否还有“加载更多”项;
  • 输入计数 text.length > 0。

没必要给纯字符串模板(如 Text("count=$count"))加 derivedStateOf,Compose 的快照系统已经足够智能。

CompositionLocal:隐式共享

CompositionLocal 提供“隐式向下传递”的方式,常用于主题色、字体、语言环境等“几乎所有组件都要用但参数透传太繁琐”的值。

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf

// 静态:值几乎不变,性能更好
val LocalThemeColor = staticCompositionLocalOf { Color.Black }

// 动态:值可能频繁变化
val LocalCurrentLocale = compositionLocalOf { "zh-CN" }

@Composable
fun AppRoot() {
    CompositionLocalProvider(
        LocalThemeColor provides Color.Blue,
        LocalCurrentLocale provides "en-US"
    ) {
        DeepChild()
    }
}

@Composable
fun DeepChild() {
    // 任意层级直接读取,无需层层透传参数
    val color = LocalThemeColor.current
    Box(Modifier.background(color))
}

慎用 CompositionLocal:它会让依赖关系隐式化,降低可测试性。仅用于真正的“全局环境”(主题、文字方向、ViewModelOwner)。业务数据应通过参数显式传递。

SavedStateHandle:ViewModel 级状态恢复

SavedStateHandle 是 ViewModel 用来持久化状态的组件,与 rememberSaveable 互补:

  • rememberSaveable 持久化 Composable 内部的小型 UI 状态;
  • SavedStateHandle 持久化 ViewModel 持有的状态,进程被杀后也能恢复。
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

class EditorViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // 直接读写 SavedStateHandle,进程恢复后值仍在
    var content: String
        get() = savedStateHandle.get<String>("content") ?: ""
        set(value) { savedStateHandle["content"] = value }

    fun save(text: String) {
        content = text
    }
}

Compose 中 ViewModel 通常通过 viewModel() 拿到(见下一章),其内部状态在配置变更和进程恢复时由 SavedStateHandle 兜底。

单向数据流(UDF)

Compose 的最佳实践是 单向数据流(Unidirectional Data Flow):

        状态 (State)            事件 (Event)
ViewModel ─────────> UI ─────────> ViewModel
(单一可信源)   (渲染)   (用户操作)
  • 状态从 ViewModel 流向 UI(通过 StateFlow/LiveData);
  • 事件从 UI 流向 ViewModel(调用 ViewModel 方法);
  • ViewModel 持有唯一可信的状态,UI 只渲染不持有真相。
class CounterViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() { _count.value++ }
    fun decrement() { _count.value-- }
}

@Composable
fun CounterScreen(vm: CounterViewModel = viewModel()) {
    val count by vm.count.collectAsStateWithLifecycle()
    Column {
        Text("count = $count")
        Button(onClick = vm::increment) { Text("+") }
        Button(onClick = vm::decrement) { Text("-") }
    }
}

UDF 的好处:状态可追溯、可测试、不分散,UI 是纯函数。

实战:可分页的搜索框

下面把状态提升、derivedStateOf、rememberSaveable 串起来,做一个搜索输入框(保存输入、显示计数、清空按钮仅在非空时出现):

@Composable
fun SearchScreen() {
    var keyword by rememberSaveable { mutableStateOf("") }
    // 派生状态:只有 keyword 非空时才显示清空按钮
    val showClear by remember(keyword) { derivedStateOf { keyword.isNotEmpty() } }

    Column(Modifier.padding(16.dp)) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            TextField(
                value = keyword,
                onValueChange = { keyword = it },
                modifier = Modifier.weight(1f),
                placeholder = { Text("输入关键字") }
            )
            if (showClear) {
                IconButton(onClick = { keyword = "" }) {
                    Icon(Icons.Default.Clear, contentDescription = "清空")
                }
            }
        }
        Spacer(Modifier.height(8.dp))
        Text("已输入 ${keyword.length} 个字符")
    }
}

旋转屏幕后 keyword 保留;showClear 由 keyword 派生,不会冗余重组。

常见坑与最佳实践

  1. 忘记 remember,每次重组状态丢失:mutableStateOf 必须包在 remember/rememberSaveable 里,否则每次重组都新建状态。
  2. 忘记导入 getValue/setValue:by 委托需要这两个扩展函数,否则 by mutableStateOf(0) 报红。
  3. 状态没提升,组件无法复用:表单输入组件内部持有状态,调用方拿不到值。状态该提升就提升。
  4. remember 的 key 用了不稳定对象:List、data class(未加 @Stable)等做 key 可能行为不符合预期。优先用 String/Int 等稳定类型。
  5. rememberSaveable 放了无法序列化的对象:会运行时崩溃。要么实现 Parcelable,要么提供 Saver,要么改用 remember。
  6. 滥用 CompositionLocal 传业务数据:会让组件测试困难、依赖隐式。CompositionLocal 只用于主题/环境类全局值。
  7. 在 Composable 里直接修改 ViewModel 字段:违背 UDF。应通过事件方法调用,状态只读。
  8. derivedStateOf 没包在 remember 里:每次重组都创建新的派生实例,失去缓存意义。务必 remember { derivedStateOf { ... } }。
  9. 在 remember 代码块里调用其他 Composable:remember{} 是普通 Kotlin 块,不能调用 @Composable 函数。
  10. 把 MutableState 直接暴露给外部:对外只暴露不可变 State<T> 或值,自己保留可变入口。

章节小结

本章系统讲解了 Compose 状态管理:remember 在重组间缓存值,rememberSaveable 在配置变更间持久化(需要 Parcelable 或 Saver);mutableStateOf + by 委托是声明可变状态的标准写法;状态提升让组件变成可复用的纯函数;derivedStateOf 用于派生值的高效缓存;CompositionLocal 仅用于主题等全局环境值;SavedStateHandle 让 ViewModel 级状态在进程恢复后还原;最后单向数据流(状态下传、事件上传)是组织大型 Compose 应用的黄金法则。

下一章预告

下一章将把状态管理与架构层打通:在 Compose 中使用 viewModel() 获取 ViewModel、StateFlow.collectAsStateWithLifecycle 与 LiveData.observeAsState 把响应式数据接到 UI、用 LaunchedEffect/DisposableEffect/rememberCoroutineScope 管理副作用,以及 AndroidView/ComposeView 在新旧体系互操作场景的实战。