ViewModel最佳实践:状态管理、事件处理、作用域与生命周期
大家好,我是你们的老朋友。今天我们来聊聊ViewModel——这个在MVVM架构里最核心的组件。说实话,我见过太多人把ViewModel当成一个简单的"数据容器",往里面塞各种LiveData、MutableLiveData,结果项目一复杂就乱成一锅粥。
ViewModel到底该怎么用?它的状态管理、事件处理、作用域和生命周期,这些坑我都踩过。今天我把这些经验整理出来,希望能帮你少走弯路。
一、ViewModel的核心职责
先问一个问题:ViewModel到底是干什么的?
说白了,ViewModel就是连接UI层和数据层的桥梁。它负责管理UI状态,处理用户交互,同时不直接持有任何View的引用。为什么?因为ViewModel的生命周期比Activity/Fragment长——屏幕旋转时Activity重建,但ViewModel还在。
我个人习惯把ViewModel比作"管家"。管家知道家里需要什么(状态),也知道怎么去采购(调用仓库层),但他不会亲自去搬砖(不操作View)。
二、状态管理:用StateFlow还是LiveData?
这是老生常谈的问题了。我直接说结论:新项目用StateFlow,老项目可以继续用LiveData。
为什么?LiveData有个问题——它只能在Android平台上用。而StateFlow是Kotlin协程家族的成员,跨平台、支持冷流热流、配合Flow的各种操作符简直不要太爽。
来看一个例子:
// 不推荐:直接用MutableLiveData暴露
class BadViewModel : ViewModel() {
private val _userName = MutableLiveData<String>()
val userName: LiveData<String> get() = _userName
}
// 推荐:用StateFlow配合私有MutableStateFlow
class GoodViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
data class UiState(
val userName: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
}
我在项目中遇到过一个问题:用LiveData时,如果多个Fragment共享同一个ViewModel,数据更新后所有观察者都会收到通知。用StateFlow配合stateIn操作符,可以更精细地控制数据流。
三、事件处理:一次性事件怎么搞?
嗯,这里有个经典问题:ViewModel里的事件,比如"显示Toast"、"导航到下一个页面",这些是一次性的。如果用LiveData或StateFlow,屏幕旋转后事件会重复触发。
为什么会这样?因为LiveData和StateFlow都是"状态持有者",它们会记住最后一个值。而事件不是状态——事件应该被消费一次就消失。
我建议用Channel来处理一次性事件:
class EventViewModel : ViewModel() {
private val _events = Channel<UiEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
sealed class UiEvent {
data class ShowToast(val message: String) : UiEvent()
data class NavigateTo(val route: String) : UiEvent()
}
fun onButtonClick() {
viewModelScope.launch {
_events.send(UiEvent.ShowToast("按钮被点击了"))
}
}
}
在Fragment里这样消费:
lifecycleScope.launch {
viewModel.events.collect { event ->
when (event) {
is UiEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
is UiEvent.NavigateTo -> findNavController().navigate(event.route)
}
}
}
Channel是非粘性的——新订阅者不会收到之前发送的事件。这就完美解决了一次性事件的问题。
四、作用域与生命周期:别让协程泄漏
ViewModel自带一个viewModelScope,它是Dispatchers.Main的协程作用域。当ViewModel被清除时,这个作用域里的所有协程都会被取消。
但有个坑:如果你在ViewModel里启动了协程,但没有用viewModelScope,而是用了GlobalScope或自定义的CoroutineScope,那协程就可能泄漏。
我曾经在一个项目里看到这样的代码:
// 错误示范
class LeakyViewModel : ViewModel() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun loadData() {
scope.launch {
// 网络请求...
}
}
override fun onCleared() {
super.onCleared()
// 忘了取消scope!
}
}
这个ViewModel被清除后,scope里的协程还在跑。如果网络请求回调里更新了LiveData,而Activity已经销毁,就会报空指针。
正确的做法:
class SafeViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
// 网络请求...
_uiState.update { it.copy(isLoading = false) }
}
}
}
viewModelScope会在onCleared()时自动取消所有协程,省心又安全。
五、知识体系总览
说了这么多,我们来画个图总结一下ViewModel的核心要点:
六、避坑指南
最后,分享几个我踩过的坑:
- 不要在ViewModel里暴露MutableLiveData——外部可以直接修改状态,破坏了数据流向。用
asStateFlow()或asLiveData()转成不可变类型。 - 不要用ViewModel保存大量数据——比如Bitmap、大列表。ViewModel在内存不足时会被回收,应该用Room或DataStore持久化。
- 不要在ViewModel里做耗时操作——用
viewModelScope.launch(Dispatchers.IO)切换到IO线程,但记得把结果切回Main更新UI。 - Fragment之间共享ViewModel要小心——确保它们属于同一个Activity,否则会拿到不同的实例。
好了,关于ViewModel的最佳实践就聊到这里。记住:ViewModel不是万能的,但用好了它,你的代码会变得清晰、可测试、易维护。下次我们聊聊Repository层的设计,看看怎么把数据源管理得井井有条。
公众号:蓝海资料掘金营,微信deep3321