多协程长耗时操作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) } } }
问题原因与优化方案
原代码问题
monitorJob方法把invokeOnCompletion注册逻辑放在了额外启动的协程里,可能导致任务已经完成,回调还没注册,最终触发失败。@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
相关产品推荐
相关产品推荐

