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 的 scope 等于界面「真正死亡」的时刻,而不是临时重建。

ViewModel 的生命周期优势

我用张图说明白 ViewModel 和 Activity 生命周期的区别。

ViewModel vs Activity 生命周期对比 Activity: onCreate() Activity: onStart() Activity: onResume() Activity: onDestroy() ViewModel: init / start ViewModel: 持续存活 ViewModel: 持续存活 ViewModel: onCleared() 绑定 Activity 重建时 ViewModel 不灭 关键差异:Activity 可能因配置变更而重建,但 ViewModel 继续存活 直到 Activity 真正 finish() 或 进程被杀 才会触发 onCleared()

从图里可以清晰看到:Activity 重建时,ViewModel 保持活跃。这意味着 ViewModel 里的协程不会因为旋转屏幕而被取消,Flow 的收集可以继续。

个人习惯:我所有需要持续观察的 Flow,都会在 ViewModel 的 init 块里用 viewModelScope 收集,而不是在 Activity 里开 lifecycleScope。

在 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 的 onCreate 里启动一个协程收集数据库 Flow,忘了取消。后来用户反馈 App 越来越卡,用 Profiler 一看,泄漏了好几个协程。就是因为旋转屏幕时旧协程没取消,新协程又开始,成倍增长。

正确的做法: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

避坑指南

要点:
  1. ViewModel 里的 Flow 收集一定要用 viewModelScope,不要自己 new Job。
  2. Activity 里的 collect 要配合 lifecycleScope 和 Lifecycle.repeatOnLifecycle,避免在 onDestroy 后收到事件。
  3. StateFlow 的 initial value 要合理设置,否则观察者可能会收到不期望的默认值。
  4. SharedFlow 记得配置 replay 和 extraBufferCapacity,避免丢事件。

我刚才提到的「Activity 收集泄漏」那个坑,后来我查了源码才发现:lifecycleScope 在 View 被销毁时确实会取消,但如果你在 onResume 里 launch 却没有保存 Job 引用,旋转屏幕后新的 Job 又启动了,老的还没取消(因为 onPause 不会自动取消 lifecycleScope)。所以把收集逻辑移到 ViewModel 才是根本解决之道。

总结

ViewModel 与 Activity 绑定的核心价值,就是让 Flow 订阅脱离 Activity 的生命周期干扰。你只需要记住:ViewModel 负责数据流管理,Activity 只负责观察和展示。这样既利用了 ViewModel 的生命周期优势,又避免了协程泄漏。我在新版项目中已经全面采用这种模式,代码清爽多了,崩溃率也降了。