ViewModel 与 Activity 绑定:生命周期优势与 Flow 安全使用
这个问题我当年踩过坑。记得刚用协程配合 LiveData 的时候,我直接在 Activity 里开了个全局的 job,结果旋转屏幕后…嗯,你们懂的——泄漏了。后来研究了一下 ViewModel 的生命周期,才彻底明白为什么要用它来承载 Flow。
为什么选择 ViewModel?
Activity 或 Fragment 的销毁不是单纯的 onDestroy。Configuration change 会导致重建,但 ViewModel 却保留在内存里。换句话说,ViewModel 是天然的「生命周期感知容器」。
你可以把 ViewModel 理解成一个跟 Activity 同生死的对象。
- Activity 执行 onDestroy(非配置改变)时,ViewModel 才会被清除。
- 旋转屏幕时,ViewModel 不会销毁,只是 Activity 重新绑定它。
所以,把 Flow 订阅放在 ViewModel 里,能避免很多手动取消的麻烦。
ViewModel 的生命周期优势
我用张图说明白 ViewModel 和 Activity 生命周期的区别。
从图里可以清晰看到:Activity 重建时,ViewModel 保持活跃。这意味着 ViewModel 里的协程不会因为旋转屏幕而被取消,Flow 的收集可以继续。
在 ViewModel 中安全使用 Flow
有了生命周期优势,具体怎么用?我整理了几个关键模式。
模式一:使用 viewModelScope
ViewModel 自带一个 CoroutineScope,叫 viewModelScope。当 onCleared() 被调用时,scope 会自动取消。直接在 ViewModel 里用就行,不需要手动管理 job。
class DataViewModel(private val repository: Repository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
// 在 viewModelScope 中安全收集 Flow
viewModelScope.launch {
repository.dataFlow.collect { data ->
_uiState.value = UiState.Success(data)
}
}
}
}
你想想看,如果我们在 Activity 里用 lifecycleScope 收集,屏幕旋转时旧的 scope 会被取消,新的 scope 重新启动,会导致重复订阅甚至状态闪烁。而 ViewModel 里只有一次订阅,稳定得多。
模式二:避免在 Activity 中直接 collect
正确的做法:Activity 只负责观察 ViewModel 暴露的 StateFlow,不做任何收集工作。
class MainActivity : AppCompatActivity() {
private val viewModel: DataViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 只观察,不收集
lifecycleScope.launch {
viewModel.uiState.collect { state ->
// 更新 UI
}
}
}
}
这里用 lifecycleScope 观察 StateFlow 是安全的,因为 StateFlow 会感知 Lifecycle,并且当 Activity 处于 STOPPED 状态时不会发出事件(默认行为)。
模式三:使用 flatMapLatest 处理嵌套 Flow
实际项目中经常会遇到:根据某个状态切换不同的数据源 Flow。比如用户登录后切换用户数据流。
class ProfileViewModel(userRepository: UserRepository) : ViewModel() {
private val _userId = MutableStateFlow<String?>(null)
val profileFlow: Flow<Profile> = _userId
.filterNotNull()
.flatMapLatest { id -> userRepository.getProfileFlow(id) }
fun onUserChanged(newId: String) {
_userId.value = newId
}
}
flatMapLatest 会自动取消之前的 Flow,避免泄漏。配合 viewModelScope 使用,完全不用担心生命周期问题。
ViewModel+Flow 最佳实践表格
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 从数据库/网络拉取数据 | 在 ViewModel 中用 viewModelScope 收集 Flow | 在 Activity 中用 lifecycleScope 直接收集 |
| UI 事件(点击、滑动) | 使用 SharedFlow / Channel 在 ViewModel 内处理 | 在 ViewModel 外暴露 MutableStateFlow |
| 需要取消重试 | 使用 viewModelScope + catch 操作符 | 手动创建 Job 并取消(容易遗漏) |
| 跨页面共享数据 | 使用同一个 ViewModel 实例(Activity scope) | 通过全局单例持有 Flow |
避坑指南
- ViewModel 里的 Flow 收集一定要用 viewModelScope,不要自己 new Job。
- Activity 里的 collect 要配合 lifecycleScope 和 Lifecycle.repeatOnLifecycle,避免在 onDestroy 后收到事件。
- StateFlow 的 initial value 要合理设置,否则观察者可能会收到不期望的默认值。
- SharedFlow 记得配置 replay 和 extraBufferCapacity,避免丢事件。
我刚才提到的「Activity 收集泄漏」那个坑,后来我查了源码才发现:lifecycleScope 在 View 被销毁时确实会取消,但如果你在 onResume 里 launch 却没有保存 Job 引用,旋转屏幕后新的 Job 又启动了,老的还没取消(因为 onPause 不会自动取消 lifecycleScope)。所以把收集逻辑移到 ViewModel 才是根本解决之道。
总结
ViewModel 与 Activity 绑定的核心价值,就是让 Flow 订阅脱离 Activity 的生命周期干扰。你只需要记住:ViewModel 负责数据流管理,Activity 只负责观察和展示。这样既利用了 ViewModel 的生命周期优势,又避免了协程泄漏。我在新版项目中已经全面采用这种模式,代码清爽多了,崩溃率也降了。