🌰 场景:点奶茶理解界面状态
打开 App 点奶茶时,你会看到:
- 商品图片
- "珍珠奶茶 ¥15"
- 【+】按钮(用于增加数量)
- 灰色的【加入购物车】按钮
当你点了两下【+】,数量变成 2,按钮突然变亮了!
👉 核心问题:App 是怎么知道"该把按钮变亮"的?
答案:它有一个"小本本"记录当前状态------这个"小本本",在 Compose 里就是界面状态(UI State)。
📝 什么是"界面状态"?
界面状态 = 决定 UI 呈现样式的所有信息,例如:
- 商品名称、价格
- 当前选中的数量
- 按钮是否可点击
- 是否正在加载(转圈圈)
- 报错信息(如"库存不足!")
只要这些信息发生变化,界面就需要同步更新。
🔄 传统做法 vs Compose 做法
❌ 以前(XML 时代)
需要手动操作每一步:找按钮 → 调用 setEnabled(true) → 手动修改颜色 → 手动更新文字......
类比:像服务员端着托盘,你喊一句,他跑一趟。
✅ 现在(Compose 时代)
只需定义规则:"如果数量 > 0,按钮就亮;否则灰着。"
只要修改"数量",Compose 会自动重绘整个界面!
类比:告诉智能厨房"订单数量大于 0,就启动打包流程",改数字后全自动响应,无需指挥每一步。
🛠 界面状态的"管家":ViewModel
想象 App 有个专职管家(ViewModel),核心职责:
- 记住所有重要的界面状态
- 处理点单、查库存、调用网络等逻辑
- 把最新状态同步给界面
代码示例:ViewModel 托管状态
kotlin
// 管家的"小本本":定义状态结构
data class OrderUiState(
val productName: String = "珍珠奶茶",
val price: Int = 15,
val quantity: Int = 0,
val isLoading: Boolean = false,
val errorMessage: String? = null
)
// 管家本身:管理状态和业务逻辑
class OrderViewModel : ViewModel() {
private val _uiState = MutableStateFlow(OrderUiState())
val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()
fun addToCart() {
if (_uiState.value.quantity > 0) {
// 模拟加购逻辑
println("已加入 ${_uiState.value.quantity} 杯!")
}
}
fun increaseQuantity() {
_uiState.update { it.copy(quantity = it.quantity + 1) }
}
}
Compose 界面:仅负责 "按状态渲染"
kotlin
@Composable
fun OrderScreen(viewModel: OrderViewModel) {
// 👀 看一眼管家的小本本
val uiState by viewModel.uiState.collectAsState()
Box(
modifier = Modifier.padding(16.dp),
contentAlignment = Alignment.TopStart
) {
Column(
modifier = Modifier
.width(200.dp)
.padding(end = 80.dp, bottom = 80.dp)
) {
Text("${uiState.productName} ${uiState.price}", fontSize = 18.sp)
Text("数量: ${uiState.quantity}", fontSize = 16.sp,
modifier = Modifier.padding(vertical = 8.dp))
Button(
onClick = { viewModel.addToCart() }, enabled = uiState > 0
) {
Text("加入购物车", fontSize = 14.sp)
}
}
FloatingActionButton(
onClick = { viewModel.increaseQuantity() },
modifier = Modifier
.align(Alignment.BottomEnd)
.offset(x = (-16).dp, y = (-16).dp)
) {
Icon(Icons.Default.Add, contentDescription = "加一杯")
}
}
}
✨ 神奇之处:
你只要调 viewModel.increaseQuantity(),
uiState.quantity 变了 → 按钮自动变亮 → 数字自动更新!
你不用写一行"更新 UI"的代码。
💡 为什么要把状态交给 ViewModel?
- 屏幕旋转不怕丢
(Activity 重建,ViewModel 还在) - 逻辑集中,好维护
(所有"加数量""查库存"都在管家那) - 多个界面能共享
(购物车页也能看到数量) - 测试方便
(不用启动 App,直接测 ViewModel)
回顾:什么是"界面状态"?
还是那个点奶茶的 App:
- 商品名、价格
- 买了几杯
- 按钮能不能点
- 正在加载吗?出错了吗?
这些决定界面长什么样的信息,就是界面状态(UI State)。
而 Compose 的魔法是:你只管改状态,它自动更新界面!
但问题来了:这些状态,到底用什么"工具"来存和改?
下面我们就来看看------Compose 里管理界面状态的"工具箱"有哪些?
🧰 工具箱一:StateFlow + ViewModel ------ 管家的"正式小本本"
✅ 适用场景:业务数据、跨组件共享、需要持久化的状态(比如用户信息、列表数据、加载状态)
🔧 核心 API:
- MutableStateFlow() → 可修改的小本本
- StateFlow → 只读版(给 UI 看)
- viewModelScope.launch → 在管家后台做事(比如调网络)
- collectAsState() → UI 订阅管家的小本本
🛠 示例:奶茶订单状态
kotlin
sealed class SnackbarState {
// 无提示(默认状态)
object None : SnackbarState()
// 普通提示
data class ShowMessage(val message: String) : SnackbarState()
// 带操作按钮的提示
data class ShowMessageWithAction(
val message: String,
val actionLabel: String,
val onAction: () -> Unit, // 操作按钮的回调(UI 层执行)
) : SnackbarState()
}
// 1. 定义"小本本"的格式
data class OrderUiState(
val productName: String = "珍珠奶茶",
val quantity: Int = 0,
val isLoading: Boolean = false,
val error: String? = null,
)
// 2. 管家(ViewModel)保管小本本
class OrderViewModel : ViewModel() {
private val _uiState = MutableStateFlow(OrderUiState())
val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()
private val _snackbarState = MutableStateFlow<SnackbarState>(SnackbarState.None)
val snackbarState: StateFlow<SnackbarState> = _snackbarState.asStateFlow()
fun addOneCup() {
_uiState.update { it.copy(quantity = it.quantity + 1) }
}
fun placeOrder() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
// 模拟下单
delay(1000)
_snackbarState.update { SnackbarState.ShowMessage("下单成功!") }
} catch (e: Exception) {
_uiState.update { it.copy(error = "下单失败") }
} finally {
_uiState.update { it.copy(isLoading = false) }
}
}
}
// 4. 重置提示状态(UI 层操作后调用)
fun resetSnackbarState() {
_snackbarState.update { SnackbarState.None }
}
}
// 3. UI 订阅小本本
@Composable
fun OrderScreen(viewModel: OrderViewModel) {
val uiState by viewModel.uiState.collectAsState() // 👈 订阅!
// 1. Snackbar 核心状态(UI 层持有)
val snackbarHostState = remember { SnackbarHostState() }
val snackbarState = viewModel.snackbarState.collectAsStateWithLifecycle()
LaunchedEffect(snackbarState.value) {
when (val state = snackbarState.value) {
is SnackbarState.ShowMessage -> {
snackbarHostState.showSnackbar(state.message)
viewModel.resetSnackbarState()
}
is SnackbarState.ShowMessageWithAction -> {}
SnackbarState.None -> {}
}
}
if (uiState.isLoading) {
CircularProgressIndicator()
} else if (uiState.error != null) {
Text(uiState.error!!, color = Color.Red)
} else {
Button(onClick = { viewModel.addOneCup() }) {
Icon(Icons.Default.Add, "添加")
}
Button(
onClick = { viewModel.placeOrder() },
enabled = uiState.quantity > 0
) {
Text("下单 (${uiState.quantity} 杯)")
}
}
SnackbarHost(hostState = snackbarHostState)
}
注:如果viewModel.launch出现红色提示,是缺少Compose协程相关依赖,请依赖这个库:
Groovy
androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" }
💡 为什么用它?
屏幕旋转不怕丢
多个界面能共用
支持复杂逻辑(网络、数据库)
Google 官方推荐!
🧰 工具箱二:mutableStateOf + remember ------ 临时便签纸
✅ 适用场景:纯 UI 交互、不涉及业务逻辑的临时状态(比如搜索框输入、开关是否打开)
🔧 核心 API:
- mutableStateOf(initialValue) → 创建可变状态
- remember { ... } → 重组时不丢内容
- by 解构 → 写起来像普通变量
🛠 示例:搜索框
kotlin
@Composable
fun SearchBar() {
// 一张"便签纸",记着当前输入
var query by remember { mutableStateOf("") }
TextField(
value = query,
onValueChange = { query = it }, // 一改,自动刷新
label = { Text("搜奶茶...") }
)
// 实时显示结果
Text("你正在搜:$query")
}
⚠️ 注意:这张"便签纸"只在当前屏幕有效。
如果你退出再回来,内容就没了(除非用 rememberSaveable)。
🧰 工具箱三:rememberSaveable ------ 防丢便签纸
✅ 适用场景:需要在屏幕旋转、进程重建后保留的状态(比如表单填写一半)
kotlin
var comment by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = comment,
onValueChange = { comment = it },
label = { Text("留言") }
)
- 和 remember 几乎一样,但更持久
- 自动保存到 Bundle(类似 Activity 的 onSaveInstanceState)
🧰 工具箱四:derivedStateOf ------ 智能便签(只在需要时更新)
✅ 适用场景:从其他状态"算出来"的值,避免无效刷新
比如:"是否显示'回到顶部'按钮?"
kotlin
val listState = rememberLazyListState()
// 智能判断:只有滚动位置变了,才重新计算
val showScrollToTop by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 5
}
}
if (showScrollToTop) {
FloatingActionButton(onClick = { /* 滚动 */ }) {
Icon(Icons.Default.ArrowUpward, "Top")
}
}
💡 好处:即使
listState频繁变化,只要结果没变(比如一直在第 3 行),就不会触发重组,性能更好!
🆚 一张表看懂怎么选

✅ 最佳实践口诀
- 业务状态找管家(ViewModel + StateFlow)
- UI 交互用便签(remember + mutableStateOf)
- 怕丢就加 Saveable
- 算出来的用 derived,性能更稳不白刷
🎯 总结
Compose 的界面状态不是玄学,而是一套清晰的分工体系:
重要的、复杂的、要共享的状态 → 交给 ViewModel,用 StateFlow
临时的、局部的、纯 UI 的状态 → 用 remember + mutableStateOf
你只需要问自己:
"这个信息,是整个业务需要记住的,还是只是这个输入框自己用的?"
答案一出,API 自然就选对了!