ViewModel
本教程共 100 篇 · 第 78 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
78. ViewModel
本节目标:理解 ViewModel 的定位和生命周期,学会定义 ViewModel、用 StateFlow 暴露状态、绑定协程作用域、在 Compose 中获取并观察 ViewModel。
上一章架构指南里 ViewModel 是界面层的状态容器,这章讲怎么写。
ViewModel 解决什么问题
打个比方,Activity 像个临时工,系统让你下岗你就下岗(旋转屏幕、内存不足)。临时工手里的话单(数据)一交接就丢。
ViewModel 像正式员工,只要部门还在(屏幕没退出返回栈),不管底下临时工怎么换,他手里的活儿和数据都不丢。
具体来说,ViewModel 解决两个核心问题:
- 配置变化时保留状态:旋转屏幕、切换语言,Activity 重建但 ViewModel 不重建。
- 托管业务逻辑:UI 只管显示和发事件,业务逻辑放 ViewModel,可测试、可复用。
第一个 ViewModel
最简单的 ViewModel:
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value = _count.value + 1
}
fun decrement() {
_count.value = _count.value - 1
}
}
要点:
- 继承
ViewModel。 - 内部用
MutableStateFlow持有可变状态,对外暴露不可变的StateFlow。 - 业务逻辑就是改
_count的值。
Note对外暴露不可变类型是关键。如果直接暴露
MutableStateFlow,外部能随便改,状态就乱了。asStateFlow()转成只读。
在 Compose 里用
Compose 用 viewModel() 函数获取 ViewModel 实例:
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsStateWithLifecycle()
Column {
Text("当前计数:$count")
Button(onClick = { viewModel.increment() }) { Text("+1") }
Button(onClick = { viewModel.decrement() }) { Text("-1") }
}
}
要点:
viewModel()默认按作用域缓存实例,同一个 ViewModelStoreOwner 下获取的是同一个实例。collectAsStateWithLifecycle()在生命周期非活跃时自动停止收集,省电。- Composable 重组时 ViewModel 不重建,状态保留。
Tip
collectAsStateWithLifecycle来自lifecycle-runtime-compose库,比collectAsState更省电。记得加依赖:implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.x")。
在 Activity/Fragment 里用
View 体系下用 viewModels() 委托:
class MainActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 观察状态...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.count.collect { count ->
countText.text = count.toString()
}
}
}
}
}
Fragment 里要用 viewLifecycleOwner 而不是 this,避免 Fragment 视图销毁后还在收集。
ViewModel 生命周期
ViewModel 的生命周期比 Activity 长。从首次获取到 Activity finish 之间都存活:
- 旋转屏幕:Activity 重建,ViewModel 不变。
- 切换语言:同上。
- 后台返回:Activity 没销毁,ViewModel 不变。
- Activity finish:ViewModel 销毁,调用
onCleared()。 - 系统杀进程:ViewModel 也没了,需要 SavedStateHandle 持久化。
Activity 创建 -> 获取 ViewModel
|
(旋转屏幕:Activity 重建,ViewModel 保留)
|
(使用中...)
|
Activity finish -> ViewModel.onCleared()
WarningViewModel 不应该在构造函数里持有
Context、View、Resources这类和 UI 绑定的对象。ViewModel 比 Activity 活得长,持有会泄漏。要 Context 就用AndroidViewModel(持有 Application Context)。
AndroidViewModel
需要 Application Context 时用 AndroidViewModel:
class MyViewModel(app: Application) : AndroidViewModel(app) {
fun doSomething() {
val prefs = getApplication<Application>().getSharedPreferences("x", 0)
// ...
}
}
但更推荐的做法是把 Context 相关的逻辑放到 Repository,ViewModel 只依赖 Repository,便于测试。
ViewModelFactory
ViewModel 有构造参数时,要用 Factory 告诉系统怎么创建:
class UserViewModel(private val repo: UserRepository) : ViewModel() {
// ...
}
class UserViewModelFactory(private val repo: UserRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return UserViewModel(repo) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
使用:
val viewModel by viewModels<UserViewModel> {
UserViewModelFactory(UserRepository(/* ... */))
}
NoteFactory 写法啰嗦,现代项目一般用 Hilt 自动注入,下一章后面会讲。手写 Factory 适合小项目或者学习阶段。
Compose 里传参数
Compose 里带参数的 ViewModel 用 CreationExtras:
class DetailViewModel(private val itemId: String) : ViewModel() {
// ...
}
// Composable 里
@Composable
fun DetailScreen(itemId: String) {
val viewModel: DetailViewModel = viewModel(
factory = viewModelFactory {
initializer {
DetailViewModel(itemId)
}
}
)
}
或者用 Navigation Compose 的 navArgument,把参数从路由取出来传给 ViewModel。
协程作用域
ViewModel 自带 viewModelScope,是个绑定到 ViewModel 生命周期的协程作用域:
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(Loading)
val uiState = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = Loading
try {
val user = repo.getUser(id)
_uiState.value = Success(user)
} catch (e: Exception) {
_uiState.value = Error(e)
}
}
}
}
sealed interface UserUiState {
object Loading : UserUiState
data class Success(val user: User) : UserUiState
data class Error(val exception: Throwable) : UserUiState
}
ViewModel 一销毁,viewModelScope 自动取消,所有协程跟着取消,不会泄漏。
Tip
viewModelScope用的是Dispatchers.Main.immediate,主线程调度。网络、数据库的suspend函数自己会切 IO,不用手动withContext。
SavedStateHandle
进程被系统杀掉后,ViewModel 也没了。要保留状态用 SavedStateHandle:
class MyViewModel(private val state: SavedStateHandle) : ViewModel() {
var query: String
get() = state.get<String>("query") ?: ""
set(value) { state["query"] = value }
}
SavedStateHandle 数据存到 Bundle,进程重启后恢复。适合存搜索框文本、当前选中的 tab 等小量数据。
Warning
SavedStateHandle只能存可序列化的简单类型(String、Int、Boolean、Parcelable 等)。大数据用 Room 持久化,别塞 SavedStateHandle。
onCleared 清理资源
ViewModel 销毁前会调 onCleared(),用来清理资源:
class PlayerViewModel : ViewModel() {
private var player: MediaPlayer? = null
init {
player = MediaPlayer()
}
override fun onCleared() {
super.onCleared()
player?.release()
player = null
}
}
状态设计
UI 状态怎么设计很关键。推荐用密封类表示多种状态:
sealed interface UiState<out T> {
object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String) : UiState<Nothing>
}
UI 层用 when 处理:
when (val state = uiState) {
is Loading -> CircularProgressIndicator()
is Success -> ShowContent(state.data)
is Error -> ShowError(state.message)
}
Note状态类要不可变(
data class全 val)。可变状态导致重组不可预测,调试噩梦。
常见坑
- ViewModel 持有 View/Activity 引用:内存泄漏。用
AndroidViewModel或者拆到 Repository。 - Repository 返回 LiveData 给 ViewModel:LiveData 应该只在 UI 层。Repository 用 Flow。
- 直接暴露 MutableStateFlow:外部乱改。用
asStateFlow()。 - 在 ViewModel 里做主线程阻塞操作:UI 卡。用协程切 IO。
- 忘用
viewModelScope:用GlobalScope或者CoroutineScope()启动协程,无法管理,泄漏。 - Fragment 里用
this观察 LiveData:Fragment 视图销毁后还在收,崩。用viewLifecycleOwner。
小结
ViewModel 是界面层状态容器,配置变化时保留状态,托管业务逻辑。用 MutableStateFlow + asStateFlow() 暴露不可变状态,viewModelScope 启动协程,SavedStateHandle 持久化小数据,onCleared() 清理资源。Compose 里用 viewModel() 获取,collectAsStateWithLifecycle() 观察。
下一章讲 LiveData,理解它和 StateFlow 的区别。