Jetpack Compose 状态管理指南

这是 Compose 系列的第七篇,前面六篇我们认识并学会了使用各种基础组件,但组件是"死"的,数据才是"活"的。今天,我们就来彻底搞懂 Compose 中的状态管理。

一、前言

在传统的 Android View 系统中,我们会用 findViewById 获取控件实例,然后调用 setTextsetVisibility 等相关方法来手动更新UI。这种命令式的更新方式,需要我们精确控制每一步的UI变化。

Compose 则采用了声明式编程 ,其核心逻辑可以用一个公式概况:

U I = f ( s t a t e ) UI = f(state) UI=f(state)

UI仅是 状态(state) 的函数映射。我们不再需要手动操作UI,只需要操作数据。数据变了,UI自动跟着变。这也是 Compose 高效且优雅的根基。

二、什么是状态

在 Android 开发中,状态(State)是一个绕不开的核心概念。应用中任何可以随时间变化的都是状态------比如输入框中的文字、开关的开启或关闭、网络请求返回的数据,以及各种组件动画...

在任何程序中,"状态 "是指在某个特定时刻,应用中存在的所有数据的快照。在 Compose 中,状态特指那些能够触发界面重组(Recomposition) 的可观察数据对象。

当状态改变时,重新执行生成 UI 的函数。这个过程叫重组

三、一段不工作的代码

先看下面一段代码:

kotlin 复制代码
@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

代码运行效果如下所示:

运行它并在输入框里输入任意字符------你会发现界面上什么都不会发生

原因在于:OutlinedTextFieldvalue 被硬编码为空字符串,onValueChange 也是空实现。Compose 不会帮你记住用户输入了什么,也不会去主动更新UI。

要使得状态去有所改变------状态变了,UI才会变。

要让输入框正常工作,需要三样东西:

  1. 可观察的状态
  2. 记住状态(避免重组时丢失)
  3. 事件回调(更新状态)

四、认识MutableStateOf

Compose使用 State<T> 来描述一个状态,泛型 T 是状态的具体类型。State<T> 是一个可观察对象 。当它的 value 发生变化时,所有读取过这个值的 Composable 函数都会被调度进行重组

当 Composable 需要对 State 的 value 进行修改,使用 MutableState<T> 来表示可修改状态 ,其包裹的数据是一个可修改的var类型

kotlin 复制代码
@Stable
public interface MutableState<T> : State<T> {
    override var value: T // 可修改的var类型

    public operator fun component1(): T

    public operator fun component2(): (T) -> Unit
}

在 Composable 中声明状态,可以有三种写法

kotlin 复制代码
// 方式一:直接使用 .value
val nameState = remember { mutableStateOf("") }
OutlinedTextField(
    value = nameState.value,
    onValueChange = { nameState.value = it }
)

// 方式二:属性委托(推荐)⭐
var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it }
)

// 方式三:解构声明
val (name, setName) = remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { setName(it) }
)

属性委托(by)需要导入:

kotlin 复制代码
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

推荐使用方式二(属性委托),代码最简洁且易读。

现在让我们用 mutableStateOf 改写 HelloConent ,并添加Log日志输出:

kotlin 复制代码
@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by mutableStateOf("") // 添加 mutableStateOf
        Log.d(TAG, ">>>>>> HelloContent() name = " + name)

        Text(
            text = "Hello, $name!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )

        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

运行上面的代码效果如下:

运行后你会发现, TextField 里还是什么都没有,但是日志可以看到每输入一个字符, HelloContent 就调用一次且 name 一直是空内容。为什么?

出现上面的情况很好理解:我们使用了mutableStateOf,但给到的值是空字符串 。即每次重组都会重新创建一个新的状态实例,初始值始终是空字符串。

kotlin 复制代码
var name by mutableStateOf("")

五、认识 remember

使用 mutableStateOf 记住了状态,但还有一个问题:Composable函数可能会被反复执行(重组)。如果状态直接写在函数体里,每次重组都会重新创建,那状态和没记住一样!

这个时候就需要使用 remember了。Composable 首次执行时,remember 会把值缓存在组合(Composition)中,后续重组时直接返回之前的值,不会重新创建。

kotlin 复制代码
@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") } // 状态被记住
		Log.d(TAG, ">>>>>> HelloContent() name = " + name)
        
        if (name.isNotEmpty()) { // 新增一行判空处理
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

现在输入框可以正常工作了。每当用户输入,onValueChange 会更新 name ,Compose 检测到状态变化,自动重组读取了 nameTextOutlinedTextField

关于"重组"的细节 ,比如它怎么做到只刷新变化的部分、什么时候会触发全量重组,我会在下一篇文章介绍。今天你只需要记住:创建状态,必须包一层 remember

六、认识 rememberSaveable

remember 好用,但缺点也明显:它只能在重组 时保留数据,无法应对 Activity 重建 (比如旋转屏幕)或者跨进程存在等场景。

测试:在输入框中输入文字,旋转屏幕,你会发现输入内容被清空了。

请看示例效果:

kotlin 复制代码
18:23:01.820 11733-11733 MainActivity             D  >>>>>> HelloContent() name = c
18:23:02.585 11733-11733 MainActivity             D  >>>>>> HelloContent() name = co
18:23:03.319 11733-11733 MainActivity             D  >>>>>> HelloContent() name = com
18:23:04.119 11733-11733 MainActivity             D  >>>>>> HelloContent() name = comp
18:23:04.803 11733-11733 MainActivity             D  >>>>>> HelloContent() name = compo
18:23:05.603 11733-11733 MainActivity             D  >>>>>> HelloContent() name = compos
18:23:06.019 11733-11733 MainActivity             D  >>>>>> HelloContent() name = compose
18:23:08.820 11733-11733 MainActivity             D  >>>>>> HelloContent() name = 

这个时候我们就需要使用 rememberSaveable 了。它可以像 Activity 的 onSaveInstanceState 那样在进程被杀时自动保存状态,同时像onRestoreInstanceState 一样随进程重建而自动恢复。

代码修改后的示例:

kotlin 复制代码
@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by rememberSaveable { mutableStateOf("") } // 把 remember -> rememberSaveable
		Log.d(TAG, ">>>>>> HelloContent() name = " + name)
        
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

运行效果和日志输出如下:

kotlin 复制代码
18:31:00.386 12471-12471 MainActivity             D  >>>>>> HelloContent() name = c
18:31:01.102 12471-12471 MainActivity             D  >>>>>> HelloContent() name = co
18:31:01.803 12471-12471 MainActivity             D  >>>>>> HelloContent() name = com
18:31:02.502 12471-12471 MainActivity             D  >>>>>> HelloContent() name = comp
18:31:03.070 12471-12471 MainActivity             D  >>>>>> HelloContent() name = compo
18:31:03.902 12471-12471 MainActivity             D  >>>>>> HelloContent() name = compos
18:31:04.570 12471-12471 MainActivity             D  >>>>>> HelloContent() name = compose
18:31:06.716 12471-12471 MainActivity             D  >>>>>> HelloContent() name = compose
18:31:11.572 12471-12471 MainActivity             D  >>>>>> HelloContent() name = composec
18:31:12.235 12471-12471 MainActivity             D  >>>>>> HelloContent() name = composecc
18:31:13.956 12471-12471 MainActivity             D  >>>>>> HelloContent() name = composecc

只需要把 remember 换成 rememberSaveable ,现在无论旋转屏幕还是因内存不足被系统杀死后重新打开,输入内容都能完美恢复。

适用场景:用户输入、滚动状态、开关状态等 UI 相关的、需要跨配置变更保留的轻量级状态。

七、状态提升(State Hoisting)

之前的例子中,状态 name 定义在 HelloContent 内部。这就意味着 HelloContent 是一个 有状态(Stateful) 的组件------组件自己持有并管理状态。

这种写法在小示例中没问题,但在实际项目中,随着业务逻辑增多,我们不可能将所有逻辑塞进一个 Composable 中。那该如何修改?类比 Java 里方法过长我们就重构为多个短方法,自然会想到的做法就是拆分Composable ,但当你拆分Composable时才发现:子组件也需要改变数据,又该怎么办呢?

答案就是使用状态提升(State Hoisting)

状态提升(State Hoisting) 就是把状态从组件内部"提"到调用方(父组件)去,让组件变成无状态(Stateless) 的,所有状态由父组件通过参数传入,并通过回调将事件传回父组件。

在传统 Android View中,将状态从View上提到Activity或者ViewModel可以把View和逻辑解耦。

状态提升的好处:

  • 单一数据源:数据只由父组件管理,不会多处散落。
  • 组件可复用:子组件变成了"无状态组件(Stateless Composable)",给它什么数据它就展示什么,换个父组件依然能用。
  • 便于调试:所有修改都通过父组件的回调进行,数据流向清晰可追踪。

具体代码如何修改请看下面:

kotlin 复制代码
// 无状态的 HelloContent ------ 状态由外部传入
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello,$name!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = {
                Text("Name")
            }
        )
    }
}

// 父组件持有状态
@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }
    HelloContent(name = name, onNameChange = { name = it })
}

状态提升的核心原则是:把状态提升到读取和写入它的所有 Composable 之间的最低共同祖先

八、单向数据流(UDF)

状态提升的自然延伸就是单向数据流(Unidirectional Data Flow,UDF) ------这是一种设计模式,核心就两句话:

  • 状态向下流动:父组件把状态通过参数传给子组件
  • 事件向上流动 :子组件通过回调把事件传给父组件

在 Compose 中,TextField 就是 UDF 的典型体现:

  • 状态向下:value 参数把当前值传给 TextField
  • 事件向上:onValueChange 回调把用户输入传回给调用方

采用单向数据流,你可以把显示 UI 的 Composable存储、改变状态的部分彻底分离开来。

九、状态容器的演进

随着状态提升的层级越来越高,状态可能会被提到 Activity 级别甚至更高。这时候再在 Composable 参数里层层传递,代码会变得臃肿不堪。

Compose 提供了几种 状态容器(State Holder) 的选择,根据状态的职责范围来区分:

9.1 UI逻辑 vs 业务逻辑

首先要区分两种逻辑:

  • UI 逻辑:和界面展示相关的逻辑。比如:根据输入内容决定按钮是否可点、控制侧滑菜单的开关。
  • 业务逻辑:和应用数据相关的逻辑。比如:保存数据到数据库、发起网络请求、处理业务规则。

这两种逻辑对应不同的状态容器方案。

9.2 普通类 + remember:管理UI逻辑

如果状态只涉及 UI 逻辑,且生命周期和 Composable 一致,可以用一个普通类配合 remember

kotlin 复制代码
class HelloState(initialName: String = "") {
    var name by mutableStateOf(initialName)
        private set
    
    fun updateName(newName: String) {
        name = newName
    }
}

@Composable
fun rememberHelloState(initialName: String = ""): HelloState {
    return remember { HelloState(initialName) }
}

@Composable
fun HelloContent(helloState: HelloState) {
    OutlinedTextField(
        value = helloState.name,
        onValueChange = { helloState.updateName(it) }
    )
}

这种方式适合逻辑并不复杂的有状态可组合项

9.3 ViewModel:管理业务逻辑

当状态涉及业务逻辑(网络请求、数据库操作等),或者需要跨屏幕共享时,就应该使用 ViewModel

ViewModel 的优势在于:

  • 生命周期更长:不受配置变更影响,存活于整个屏幕(Destination)的生命周期
  • 与数据层交互:可以方便地调用 Repository 或 UseCase
  • 可测试性:可以单独对 ViewModel 做单元测试
kotlin 复制代码
class HelloViewModel : ViewModel() {
    private val _name = mutableStateOf("")
    val name: State<String> = _name
    
    fun updateName(newName: String) {
        _name.value = newName
    }
}

@Composable
fun HelloScreen(viewModel: HelloViewModel = viewModel()) {
    HelloContent(
        name = viewModel.name.value,
        onNameChange = { viewModel.updateName(it) }
    )
}

9.4 如何选择

状态类型 推荐方案 生命周期
单个Composable内部使用的UI状态 remember + mutableStateOf 组合(Composition)
跨配置变更保留的UI状态 rememberSaveable Activity重建
多个Composable 共享的UI逻辑 普通状态容器类 + remember 组合(Composition)
涉及业务逻辑的状态 ViewModel 当前导航目的地(即当前页面,离开即自动销毁)
需要跨导航共享的状态 ViewModel + 共享作用域 父级 NavGraph 或 Activity

十、结语

本篇内容核心总结出来就是:

  • mutableStateOf 创建可观察状态,变化时触发重组
  • remember 让状态在重组时不被重置
  • rememberSaveable 让状态在配置变更和进程终止后依然保留
  • 状态提升把状态上移到调用方,让组件变为无状态(Stateless),提高复用性
  • 单向数据流是 Compose 推荐的设计模式:状态向下,事件向上
  • 状态容器 的选择取决于逻辑类型------UI逻辑用普通类 + remember,业务逻辑用 viewModel

掌握好状态管理,是写出高质量 Compose 应用的关键一步。

十一、参考资料

十二、往期文章

  1. Jetpack Compose 前探
  2. Jetpack Compose 入门指南
  3. Jetpack Compose 核心机制:Composable与Modifier
  4. Jetpack Compose Modifier修饰符完全指南:从入门到精通
  5. Jetpack Compose 常用UI组件实战演练
  6. Jetpack Compose 组件大观园
相关推荐
爱笑鱼4 小时前
JNI(二):Java 里的一个 long,为什么能找到 C++ 对象?
android
爱笑鱼4 小时前
JNI(一):nativePollOnce() 到底去了哪里?
android
hqk6 小时前
玩Android×AI Coding:从产品到代码,完整跑通一套 AI 软件研发工作流
android·ai编程·vibecoding
安卓修改大师7 小时前
安卓修改大师实战:深入解析Android主题Theme修改与沉浸式界面实现
android
齊家治國平天下8 小时前
CarService 深度解析 —— 基于 Android 14
android·车载系统·aaos·carservice·电源管理·android 14·vhal
stevenzqzq9 小时前
app性能日志工具
android
DB哥讲数据库9 小时前
【最新】MySQL9.7安装教程:图文详解(附MySQL安装包)
android·mysql·adb
IT乐手10 小时前
Android 写本地日志工具类
android·java
非典型Android程序员11 小时前
Launcher学习总结
android