Compose 状态管理
问题
Compose 中如何管理状态?状态提升(State Hoisting)是什么?
答案
1. 状态基础
// mutableStateOf - 创建可观察状态
var name by remember { mutableStateOf("") }
// mutableIntStateOf - 基本类型优化版本(避免装箱)
var count by remember { mutableIntStateOf(0) }
// rememberSaveable - 配置变更(如旋转)后仍保持状态
var text by rememberSaveable { mutableStateOf("") }
2. 状态提升(State Hoisting)
将状态从子组件提升到父组件,使子组件成为无状态的:
// ❌ 有状态的组件(不利于复用和测试)
@Composable
fun SearchBar() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
// ✅ 无状态组件(状态提升到父组件)
@Composable
fun SearchBar(
query: String, // 状态下传
onQueryChange: (String) -> Unit // 事件上传
) {
TextField(value = query, onValueChange = onQueryChange)
}
// 父组件持有状态
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
SearchBar(query = query, onQueryChange = { query = it })
SearchResults(query)
}
状态提升原则
将状态提升到最低公共祖先:需要共享状态的 Composable 的最近公共父级。
3. ViewModel 集成
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun updateName(name: String) {
_uiState.update { it.copy(name = name) }
}
}
data class UserUiState(
val name: String = "",
val isLoading: Boolean = false
)
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
// collectAsStateWithLifecycle - 生命周期感知的状态收集
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserContent(
name = uiState.name,
isLoading = uiState.isLoading,
onNameChange = viewModel::updateName
)
}
4. 状态持有者模式
对于复杂的 UI 逻辑,使用状态持有者类(不是 ViewModel):
// 状态持有者 - 管理 UI 相关逻辑
class SearchBarState(
initialQuery: String = ""
) {
var query by mutableStateOf(initialQuery)
private set
var isExpanded by mutableStateOf(false)
private set
fun updateQuery(newQuery: String) {
query = newQuery
}
fun toggleExpand() {
isExpanded = !isExpanded
}
}
@Composable
fun rememberSearchBarState(initialQuery: String = "") = remember {
SearchBarState(initialQuery)
}
@Composable
fun SearchScreen() {
val searchState = rememberSearchBarState()
SearchBar(state = searchState)
}
5. derivedStateOf
当状态需要从其他状态派生时使用,避免不必要的重组:
@Composable
fun FilteredList(items: List<String>) {
var query by remember { mutableStateOf("") }
// ✅ 只在 items 或 query 变化时重新计算
val filteredItems by remember(items) {
derivedStateOf {
items.filter { it.contains(query, ignoreCase = true) }
}
}
TextField(value = query, onValueChange = { query = it })
LazyColumn {
items(filteredItems) { item -> Text(item) }
}
}
常见面试问题
Q1: remember 和 rememberSaveable 的区别?
答案:
remember:在重组时保持值,但 Activity 重建(如旋转屏幕)时丢失rememberSaveable:在 Activity 重建和进程死亡后恢复时也能保持值(内部使用SavedStateHandle)
rememberSaveable 默认支持 Bundle 能保存的类型。自定义类型需要实现 Saver 或使用 @Parcelize。
Q2: Compose 中应该在哪里管理状态?
答案:
| 状态类型 | 位置 | 示例 |
|---|---|---|
| 临时 UI 状态 | Composable 内 remember | TextField 输入、动画状态 |
| 屏幕级 UI 状态 | 状态持有者类 + remember | Scaffold 状态、搜索栏展开 |
| 业务逻辑状态 | ViewModel + StateFlow | 网络数据、用户信息 |
| 跨屏幕状态 | ViewModel + Navigation | 登录状态、全局设置 |
Q3: collectAsState 和 collectAsStateWithLifecycle 的区别?
答案:
collectAsState():始终收集 Flow,即使 App 在后台collectAsStateWithLifecycle():只在生命周期至少是STARTED时才收集。App 进入后台时自动停止收集,节省资源
推荐始终使用 collectAsStateWithLifecycle()。
Q4: 什么是单向数据流(Unidirectional Data Flow)?
答案:
Compose 推荐的状态管理模式:状态向下流动,事件向上流动。
ViewModel → State ↓ UI Composable → Event ↑ ViewModel
- 状态(State):从 ViewModel 向下传递给 Composable
- 事件(Event):从 Composable 向上传递给 ViewModel(通过 lambda 回调)
好处:状态变化可追溯、UI 可预测、易于测试。
Q5: snapshotFlow 的作用是什么?
答案:
snapshotFlow 将 Compose 状态转换为 Flow,当状态变化时发射新值:
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index ->
// 监听滚动位置变化
analytics.trackScroll(index)
}
}
适用于需要将 Compose 状态桥接到 Flow 链式处理的场景。