Jetpack Compose 中的状态
一、什么是"状态"?
应用里可以随时间变化的任何值 都叫"状态"------这个定义很宽泛,小到一个按钮的点击状态、输入框的文本,大到数据库里的博文和评论、网络连接状态,都属于"状态"。
Android 应用本质上就是在"展示状态":比如没网时显示的提示、点击按钮时的涟漪效果、图片上用户添加的贴纸,本质都是在呈现不同时刻的"状态"。
二、状态和"组合/重组"的关系
Compose 是"声明式"工具集,和传统 XML 布局的"命令式"完全不同------它更新界面的唯一方式,是用新参数重新调用可组合项 (也就是那些带 @Composable
注解的函数)。这些参数,其实就是界面状态的"表现形式"。
这里要先搞懂三个关键术语:
- 组合:Compose 执行可组合项后,生成的"界面描述"(相当于把代码翻译成了手机能显示的界面结构);
- 初始组合:第一次运行可组合项,创建出初始界面的过程;
- 重组:当状态变化时,重新运行可组合项、更新"组合"(也就是更新界面)的过程。
举个直观例子, HelloContent
函数:
kotlin
@Composable private fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "Hello!", ...)
OutlinedTextField(
value = "", // 固定空字符串,没有关联状态
onValueChange = { }, // 没处理输入变化
label = { Text("Name") }
)
}
}
运行后输入文本没反应------因为 TextField
不会"自动更新":它的 value
参数是固定空字符串,状态没变化,就不会触发"重组",界面自然不变。
这就是 Compose 的核心规则:只有状态变了,且可组合项用到了这个状态,才会触发重组更新界面。
三、可组合项里怎么存状态?(2个核心API)
要让界面能跟着状态变,得解决两个问题:① 把状态存起来;② 让状态变化能触发重组。Compose 提供了两个关键 API 配合解决:
1. remember:负责"存储状态"
remember
是个"记忆工具",作用是把对象存储在"组合"里:
- 初始组合时,计算一个值并存起来;
- 重组时,直接返回之前存储的值(不会重新计算);
- 注意:如果调用
remember
的可组合项被从界面中移除(比如切换页面时),remember
会"忘记"存储的值(相当于临时缓存,随界面生命周期变化)。
它既能存不可变对象(比如固定的字符串),也能存可变对象------但要配合下面的 API 才能触发重组。
2. mutableStateOf:负责"让状态可观察"
mutableStateOf
会创建一个 MutableState<T>
类型的对象,这是 Compose 内置的"可观察类型",核心特点是:
- 它是个接口,核心是
var value: T
(可以修改值); - 只要修改
value
的值,所有"读取过这个 value"的可组合项,都会被自动安排"重组"(也就是更新界面)。
简单说:remember
负责"存住状态",mutableStateOf
负责"让状态变化能被 Compose 感知",两者搭配才能实现"状态变 → 界面变"。
四、声明 MutableState 的3种方式(语法糖,按需选)
mutableStateOf
通常和 remember
一起用,有3种等效写法,只是语法不同,目的是让代码更易读:
特性 | val nameState = remember { mutableStateOf("") } | var name by remember { mutableStateOf("") } | val (name, setName) = remember { mutableStateOf("") } |
---|---|---|---|
变量类型 | val (不可变引用) |
var (可变属性) |
val (两个不可变引用) |
访问值 | nameState.value |
name |
name |
修改值 | nameState.value = "New" |
name = "New" |
setName("New") |
本质 | 直接持有 State 对象 |
使用属性委托 ,编译器自动处理 .value |
使用解构声明 |
代码风格 | 显式,能清楚看到状态对象 | 简洁,类似普通的可变变量 | 类似于 React Hooks 的风格 |
需要导入 | 无特殊导入 | import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue |
无特殊导入 |
适用场景 | 需要将 State 对象本身传递给其他函数时 |
最常用、最推荐的写法,代码简洁直观 | 习惯函数式风格,希望将值和设置器分开 |
在实际开发中,第二种方式 (by
委托) 因其极高的可读性和简洁性而成为社区和官方最推崇的标准写法(传递的是值)。第一种方式在需要传递状态引用时很有用。第三种方式则提供了一种不同的代码风格,适合那些喜欢显式分离"值"和"更新函数"的开发者。
五、传递对象和传递值的区别
传递状态对象和传递值在 Compose 中有本质的区别,这关系到重组机制 和数据流方向。
1. 核心区别
特性 | 传递值 | 传递状态对象 |
---|---|---|
传递的内容 | 当前的数据值 | 包含数据和更新能力的对象 |
接收方能否修改 | ❌ 不能 | ✅ 能 |
重组范围 | 传递方重组 → 接收方重组 | 接收方可独立重组 |
数据流 | 单向数据流 | 双向数据流 |
2. 具体例子说明
例子1:传递值(单向数据流)
kotlin
@Composable
fun ParentComponent() {
var name by remember { mutableStateOf("") }
Column {
// 传递值给子组件
ChildComponentReadOnly(value = name)
// 父组件自己处理修改
TextField(
value = name,
onValueChange = { name = it }
)
}
}
@Composable
fun ChildComponentReadOnly(value: String) {
// 子组件只能读取值,不能修改
Text("Hello, $value!")
// 如果尝试修改会编译错误:
// value = "New" // ❌ 编译错误!
}
特点:
- 数据流是单向的:父组件 → 子组件
- 子组件是纯展示的,没有副作用
- 状态管理完全由父组件控制
例子2:传递状态对象(双向数据流)
kotlin
@Composable
fun ParentComponent() {
val nameState = remember { mutableStateOf("") }
Column {
// 传递状态对象给子组件
ChildComponentWithState(state = nameState)
// 父组件也能看到子组件的修改
Text("Parent sees: ${nameState.value}")
}
}
@Composable
fun ChildComponentWithState(state: MutableState<String>) {
TextField(
value = state.value,
onValueChange = { state.value = it }
)
// 子组件可以直接修改状态
Button(onClick = { state.value = "Reset" }) {
Text("Reset")
}
}
特点:
- 数据流是双向的:父组件 ↔ 子组件
- 子组件可以直接修改状态
- 双方都能实时看到对方的修改
3. 重组行为的区别
传递值的情况:
kotlin
@Composable
fun Parent() {
var count by remember { mutableStateOf(0) }
// 当count变化时,Parent会重组
// Child也会重组,因为它接收了新的值
Child(value = count)
Button(onClick = { count++ }) {
Text("Increment")
}
}
@Composable
fun Child(value: Int) {
Text("Count: $value") // 接收新值,会重组
}
传递状态对象的情况:
kotlin
@Composable
fun Parent() {
val countState = remember { mutableStateOf(0) }
// Parent不会重组,因为countState引用没变
// Child自己处理重组
Child(state = countState)
Button(onClick = { countState.value++ }) {
Text("Increment")
}
}
@Composable
fun Child(state: MutableState<Int>) {
Text("Count: ${state.value}") // 自己读取状态,自己重组
}
4. 小结
方面 | 传递值 | 传递状态对象 |
---|---|---|
控制权 | 集中控制 | 分散控制 |
数据流 | 单向 | 双向 |
组件职责 | 展示职责 | 业务逻辑职责 |
测试难度 | 容易测试 | 较难测试 |
推荐程度 | ✅ 优先使用 | 🔶 特定场景使用 |
最佳实践 :优先使用传递值的方式,遵循单向数据流原则。只有在子组件确实需要直接修改状态,且这种修改是合理的业务需求时,才考虑传递状态对象。
六、实际用法:让状态控制界面
状态的核心作用,是"决定界面该显示什么"。比如优化版的 HelloContent
:
kotlin
@Composable fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
// 用 by 委托语法,声明并存储"姓名状态",初始值为空
var name by remember { mutableStateOf("") }
// 状态控制界面:只有姓名不为空时,才显示问候语
if (name.isNotEmpty()) {
Text(text = "Hello, $name!") // 用到了 name 状态
}
OutlinedTextField(
value = name, // 绑定状态:输入框的文本 = 姓名状态
onValueChange = { name = it }, // 输入变化时,更新状态
label = { Text("Name") }
)
}
}
这里的逻辑链很清晰:
- 输入框输入文本 →
onValueChange
回调更新name
状态; name
状态变化 → 触发用到name
的可组合项(if
里的Text
和TextField
本身)重组;- 重组时,
if (name.isNotEmpty())
条件生效,显示问候语;输入框也同步显示新的name
值。
这就是 Compose 处理状态的核心流程:状态存储(remember)→ 状态观察(mutableStateOf)→ 状态变化触发重组 → 界面更新。