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

多协程长耗时操作UI单Spinner状态控制:invokeOnCompletion未触发

问题描述

我的ViewModel里有多个启动协程执行耗时后台操作的函数,UI中有个Spinner需要在至少一个后台进程活跃时显示。我尝试用计数器加invokeOnCompletion实现,但回调函数始终没触发,求最优实现方式。


现有代码

ViewModel代码

class DeviceListViewModel() : ViewModel() {

    private val _updating = Updating(viewModelScope)
    val updating = _updating.state

    fun refresh1(){
        viewModelScope.launchUpdating{

        }
    }

    fun refresh2(){
        viewModelScope.launchUpdating{

        }
    }

    private fun CoroutineScope.launchUpdating(block: suspend CoroutineScope.() -> Unit) =
        launchUpdating(this@launchUpdating, _updating, block)
}

Updating类代码(存在回调未触发问题)

/**
 * 用于监控多个任务的运行状态
 * 只要至少有一个任务活跃,状态值就为TRUE
 * 只有当所有任务都完成时,状态才自动改为FALSE
 */
class Updating(private val scope: CoroutineScope) {
    private val isUpdating = MutableStateFlow(false)
    @Volatile private var counter = 0
    private val mutex = Mutex()

    val state = isUpdating.asStateFlow()

    private fun monitorJob(job: Job) {
        scope.launch {
            mutex.withLock{
                counter++
                if (counter > 0){
                    isUpdating.value = true
                }
                job.invokeOnCompletion(this@Updating::onJobCompletion)
            }
        }
    }

    @Suppress("UNUSED_PARAMETER")
    private fun onJobCompletion(th:Throwable?) {
        scope.launch {
            mutex.withLock{
                counter--
                if (counter < 0){
                    counter = 0
                }
                if (counter == 0){
                    isUpdating.value = false
                }
            }
        }
    }

    companion object {
        fun launchUpdating(scope: CoroutineScope, updating: Updating, block: suspend CoroutineScope.() -> Unit) =
            scope.launch(block = block).also { updating.monitorJob(it) }
    }
}

问题原因与优化方案

原代码问题

  1. monitorJob方法把invokeOnCompletion注册逻辑放在了额外启动的协程里,可能导致任务已经完成,回调还没注册,最终触发失败。
  2. @Volatile修饰计数器属于冗余操作,Mutex已经保证了线程安全,两者无需同时使用。

最优实现方式:计数器方案(简化版)

直接在任务启动时同步注册回调,用原子类+Mutex确保线程安全,逻辑更可靠:

class Updating {
    private val isUpdating = MutableStateFlow(false)
    private val counter = AtomicInteger(0)
    private val mutex = Mutex()

    val state = isUpdating.asStateFlow()

    fun launchUpdating(scope: CoroutineScope, block: suspend CoroutineScope.() -> Unit) {
        scope.launch {
            // 任务启动时更新计数器与状态
            mutex.withLock {
                if (counter.incrementAndGet() == 1) {
                    isUpdating.value = true
                }
            }
            try {
                block() // 执行耗时操作
            } finally {
                // 任务结束后更新计数器与状态
                mutex.withLock {
                    if (counter.decrementAndGet() == 0) {
                        isUpdating.value = false
                    }
                }
            }
        }
    }
}

对应的ViewModel修改:

class DeviceListViewModel : ViewModel() {

    private val _updating = Updating()
    val updating = _updating.state

    fun refresh1() {
        _updating.launchUpdating(viewModelScope) {
            // 这里写具体的耗时后台操作
        }
    }

    fun refresh2() {
        _updating.launchUpdating(viewModelScope) {
            // 这里写具体的耗时后台操作
        }
    }
}

替代方案:直接跟踪活跃Job

如果想更直观地管理任务,也可以直接维护活跃Job集合,逻辑更清晰:

class Updating {
    private val activeJobs = mutableSetOf<Job>()
    private val isUpdating = MutableStateFlow(false)
    private val mutex = Mutex()

    val state = isUpdating.asStateFlow()

    fun launchUpdating(scope: CoroutineScope, block: suspend CoroutineScope.() -> Unit) {
        val job = scope.launch { block() }
        
        mutex.withLock {
            activeJobs.add(job)
            isUpdating.value = true
        }

        job.invokeOnCompletion {
            mutex.withLock {
                activeJobs.remove(job)
                if (activeJobs.isEmpty()) {
                    isUpdating.value = false
                }
            }
        }
    }
}

这种方式避免了计数器可能出现的异常(比如负数),直接通过集合的空判断控制Spinner显示状态。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 03:16:15