jetpack compose 中的状态
一、什么是“状态”?
应用里可以随时间变化的任何值都叫“状态”——这个定义很宽泛,小到一个按钮的点击状态、输入框的文本,大到数据库里的博文和评论、网络连接状态,都属于“状态”。
android 应用本质上就是在“展示状态”:比如没网时显示的提示、点击按钮时的涟漪效果、图片上用户添加的贴纸,本质都是在呈现不同时刻的“状态”。
二、状态和“组合/重组”的关系
compose 是“声明式”工具集,和传统 xml 布局的“命令式”完全不同——它更新界面的唯一方式,是用新参数重新调用可组合项(也就是那些带 @composable 注解的函数)。这些参数,其实就是界面状态的“表现形式”。
这里要先搞懂三个关键术语:
- 组合:compose 执行可组合项后,生成的“界面描述”(相当于把代码翻译成了手机能显示的界面结构);
- 初始组合:第一次运行可组合项,创建出初始界面的过程;
- 重组:当状态变化时,重新运行可组合项、更新“组合”(也就是更新界面)的过程。
举个直观例子, hellocontent 函数:
@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.getvalueimport androidx.compose.runtime.setvalue | 无特殊导入 |
| 适用场景 | 需要将 state 对象本身传递给其他函数时 | 最常用、最推荐的写法,代码简洁直观 | 习惯函数式风格,希望将值和设置器分开 |
在实际开发中,第二种方式 (by 委托) 因其极高的可读性和简洁性而成为社区和官方最推崇的标准写法(传递的是值)。第一种方式在需要传递状态引用时很有用。第三种方式则提供了一种不同的代码风格,适合那些喜欢显式分离“值”和“更新函数”的开发者。
五、传递对象和传递值的区别
传递状态对象和传递值在 compose 中有本质的区别,这关系到重组机制和数据流方向。
1. 核心区别
| 特性 | 传递值 | 传递状态对象 |
|---|---|---|
| 传递的内容 | 当前的数据值 | 包含数据和更新能力的对象 |
| 接收方能否修改 | ❌ 不能 | ✅ 能 |
| 重组范围 | 传递方重组 → 接收方重组 | 接收方可独立重组 |
| 数据流 | 单向数据流 | 双向数据流 |
2. 具体例子说明
例子1:传递值(单向数据流)
@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:传递状态对象(双向数据流)
@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. 重组行为的区别
传递值的情况:
@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") // 接收新值,会重组
}传递状态对象的情况:
@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:
@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)→ 状态变化触发重组 → 界面更新。
到此这篇关于android compose 状态的概念的文章就介绍到这了,更多相关android compose 状态内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论