003 状态管理与 ViewModel:我的Compose实战心得
1. 从「改 View」到「改状态」的思维转变
刚开始接触 Compose 时,我总是不自觉地想用 XML 时代的老方法:editText.setText(s)、button.isEnabled = flag,总想着直接操作 UI 组件。但 Compose 彻底改变了这个逻辑——它让我明白,UI 应该是状态的函数。
Compose 的核心逻辑是:Composable 读取状态 → 状态变化 → 相关 Composable 自动重组。所以我的工作重心,从直接操作 View 变成了如何管理好数据状态,并遵循 单向数据流 的原则(事件向上传递,状态向下流动)。这个思维转变,是我上手 Compose 后最大的收获之一。
2. 我的入门搭档:remember + mutableStateOf
对于界面局部、只在当前重组范围内需要保持的状态(比如一个控件的展开/收起状态、输入框的草稿内容),我通常会用 remember 和 mutableStateOf 这对组合拳。
import androidx.compose.runtime.*
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) }
Column(modifier = Modifier.padding(16.dp)) {
Text("次数: $count")
Button(onClick = { count++ }) { Text("+1") }
}
}
这里有几个关键点是我在实践中总结的:
- remember 是关键:它保证在重组后,状态实例能被保留下来。如果没有它,每次重组 count 都会重新被初始化为 0,状态就丢了。
- by 委托:这可不是语法糖那么简单。它让 count 的读写都通过 MutableState 的 value 属性进行,而赋值操作会自动触发重组,非常方便。
另外一个小贴士:如果你的项目 Kotlin 版本在 1.9+,推荐使用 mutableIntStateOf 这类具体类型的方法,可以避免不必要的装箱开销。老项目用 mutableStateOf(0) 也完全没问题。
3. 状态提升(State Hoisting):让组件更纯粹
随着组件变复杂,我很快发现把状态都写在组件内部会让测试和复用变得困难。这时就需要 状态提升——把状态提升到调用方,让子组件变成**无状态(stateless)**的纯展示组件。这其实和 MVVM 中 View 只负责展示和发送事件 的思想不谋而合。
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
onSearch: () -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier.weight(1f),
label = { Text("关键词") }
)
Button(onClick = onSearch, modifier = Modifier.padding(start = 8.dp)) {
Text("搜索")
}
}
}
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
SearchBar(
query = query,
onQueryChange = { query = it },
onSearch = { /* 用 query 调接口 */ }
)
}
这样做之后,SearchBar 变得非常“纯粹”,我可以轻松地给它不同的初始值,或者在不同的场景复用,预览也变得简单。
4. ViewModel + Compose:业务状态的归宿
对于业务状态,或者需要跨配置变更(比如旋转屏幕)保持的状态,remember 就不够用了。这时候就该 ViewModel 出场了——它和我们在 Fragment/Activity 中使用的方式一脉相承。
依赖配置(以当前版本为例)
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
这里注意,lifecycle-runtime-compose 提供了 collectAsState() 等扩展,让我们能在 Composable 中方便地收集 StateFlow。
在 Composable 中获取 ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun UserScreen(vm: UserViewModel = viewModel()) {
val uiState by vm.uiState.collectAsState()
// 根据 uiState 渲染;事件调用 vm.onXxx()
}
在 ViewModel 内部,我习惯用 StateFlow / MutableStateFlow 来暴露 UI 状态,感觉比 LiveData 更符合 Kotlin 协程的生态:
data class UserUiState(val name: String = "", val loading: Boolean = false)
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun load() {
viewModelScope.launch {
_uiState.update { it.copy(loading = true) }
// ... 模拟网络请求
_uiState.update { it.copy(loading = false, name = "Alice") }
}
}
}
在 Composable 中收集状态就一行代码:val state by vm.uiState.collectAsState()。记得确保相关依赖版本对齐。
5. 与 LiveData 的衔接(给迁移中的项目)
如果你的老项目还在大量使用 LiveData,也不用急着全部重写。可以用 observeAsState()(来自 androidx.compose.runtime:runtime-livedata)来桥接,逐步迁移到 StateFlow。
6. 我踩过的坑:反模式提示
- 在 Composable 里直接
val list = mutableListOf()且不用remember:重组时数据会丢失,或者列表被重复初始化,导致奇怪的问题。 - 把非 UI 的副作用(网络请求、读写数据库)直接写在 Composable 函数体里:这是大忌!应该放到
LaunchedEffect或者 ViewModel 中处理(这部分我们会在第 008 篇详细讲)。
7. 动手练习
光说不练假把式,我建议你动手试试:
- 尝试把「登录页」拆成:无状态的
LoginForm(email, password, onEmailChange, ...),然后在父级用remember持有输入状态。 - 用
ViewModel持有一个包含loading和errorMessage的状态,模拟按钮点击触发“假请求”,并在界面上正确显示加载状态和错误信息。
实践出真知,写一遍印象会深刻很多。
下一篇,我们来聊聊列表:004-列表LazyColumn与RecyclerView对照.md
当前文章价值6.13元,扫一扫支付后添加微信提供帮助!(如不能解决您的问题,可以申请退款)

评论已关闭!