You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

屏幕旋转时避免ViewModel重复执行的优化方案咨询

问题描述

从Fragment中调用ViewModel的方法后,屏幕旋转时该方法会被再次执行。希望保留已创建的ViewModel及其内部值,避免销毁。已知可以用Coroutines Job处理,想咨询更优实现方案;同时不想通过SavedStateHandle销毁现有ViewModel并重新获取数据。

ViewModel 代码

private var _counterViewModelLiveData = MutableLiveData(0)
val counterViewModelLiveData
    get() = _counterViewModelLiveData
private var job: Job? = null

fun startVmCounter() {
    job?.cancel()
    job = viewModelScope.launch {
        while (true) {
            delay(TimeUnit.SECONDS.toMillis(1))
            _counterViewModelLiveData.value = _counterViewModelLiveData.value!! + 1
        }
    }
}

Fragment 代码

private fun viewModelSaveState(){
    vm.startVmCounter()
    vm.counterViewModelLiveData.observe(viewLifecycleOwner){
        binding.txtViewmodelSaveStateCounter.text = it.toString()
    }
}

解决方案

核心问题拆解

屏幕旋转属于配置变更,此时Fragment会重建,导致viewModelSaveState()被重复调用,进而触发startVmCounter()重复执行。但ViewModel本身在配置变更时不会被销毁,所以问题本质是Fragment重建后的重复触发,而非ViewModel销毁。

更优实现方案

1. ViewModel内部维护运行状态,避免重复启动

在ViewModel中添加任务运行状态标记,调用启动方法时先判断状态,已运行则直接返回:

private var _counterViewModelLiveData = MutableLiveData(0)
val counterViewModelLiveData
    get() = _counterViewModelLiveData
private var job: Job? = null
private var isCounterRunning = false // 新增运行状态标记

fun startVmCounter() {
    if (isCounterRunning) return // 已运行则跳过
    job?.cancel()
    isCounterRunning = true
    job = viewModelScope.launch {
        try {
            while (true) {
                delay(TimeUnit.SECONDS.toMillis(1))
                _counterViewModelLiveData.value = _counterViewModelLiveData.value!! + 1
            }
        } finally {
            isCounterRunning = false // 任务取消/结束时重置标记
        }
    }
}

这种方式将逻辑内聚在ViewModel中,无论Fragment多少次重建,重复调用startVmCounter()都不会重启协程,同时保留原有的计数状态。

2. Fragment层控制仅首次启动任务

利用Fragment的生命周期特性,确保任务只在View首次创建时启动一次:

private var isFirstLaunch = true

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // 先绑定观察者
    vm.counterViewModelLiveData.observe(viewLifecycleOwner){
        binding.txtViewmodelSaveStateCounter.text = it.toString()
    }
    // 仅首次创建View时启动计数
    if (isFirstLaunch) {
        vm.startVmCounter()
        isFirstLaunch = false
    }
}

该方案轻量,适合单一启动入口的场景,但需注意:若Fragment因进程被杀后恢复,isFirstLaunch会重置,此时若需保留启动状态则需结合SavedStateHandle,但你明确不想使用,因此该方案更适合纯配置变更场景。

3. 使用Flow+stateIn实现自动持续计数(推荐)

用Flow替代LiveData,借助stateIn将冷Flow转为热Flow,利用Jetpack组件特性减少手动维护代码:

// ViewModel中
val counterFlow = flow {
    var count = _counterViewModelLiveData.value ?: 0
    while (true) {
        delay(1000)
        count++
        emit(count)
    }
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000), // 订阅存在时保持活跃
    initialValue = _counterViewModelLiveData.value ?: 0
)

// Fragment中
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            vm.counterFlow.collect { count ->
                binding.txtViewmodelSaveStateCounter.text = count.toString()
            }
        }
    }
}

此方案无需手动管理Job和状态标记,stateIn创建的热Flow会在ViewModel生命周期内持续计数,Fragment重建后重新收集Flow时会直接获取当前最新值,且不会重复启动计数逻辑,代码简洁可靠。

方案对比

  • ViewModel内部状态标记:逻辑内聚,适合多入口调用启动方法的场景。
  • Fragment首次启动控制:轻量简洁,适合单一启动入口的纯配置变更场景。
  • Flow+stateIn:利用Jetpack原生特性,减少手动维护成本,是最推荐的方案。

内容的提问来源于stack exchange,提问作者Ali Doran

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 10:15:33