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

纯Kotlin实现可重启倒计时遇问题:无法终止旧任务重启

问题分析

你的倒计时重启时旧任务未停止的核心问题有两个:

  1. toggleTime 中的 coroutineScope 会挂起函数直到内部协程完成,导致首次调用后函数一直阻塞,后续重启调用的时机和取消逻辑被干扰。
  2. initTimer 中使用序列的 filter 判断作用域活性是无效的——协程取消时,delay(1000) 会直接抛出 CancellationException 终止流收集,根本轮不到序列过滤执行。
修复后的代码
class CountDownTimerUseCase {

    private val _countDown = MutableStateFlow(0)
    val countDown: StateFlow<Int> = _countDown

    private var countDownTimerJob: Job? = null

    suspend fun toggleTime(totalSeconds: Int) {
        // 先取消并等待旧任务结束,避免并发问题
        countDownTimerJob?.cancelAndJoin()

        // 直接在当前上下文启动新协程,无需额外创建coroutineScope
        countDownTimerJob = coroutineScope {
            launch {
                initTimer(totalSeconds)
                    .onCompletion { cause ->
                        // 只有正常结束或取消时才重置为0,避免异常情况下重复设置
                        if (cause == null || cause is CancellationException) {
                            _countDown.emit(0)
                        }
                    }
                    .collect { _countDown.emit(it) }
            }
        }
    }

    private fun initTimer(totalSeconds: Int): Flow<Int> = flow {
        // 立即发射总时长
        emit(totalSeconds)
        // 从总时长-1倒序发射,每秒一次
        repeat(totalSeconds) {
            // 每次发射前检查当前协程是否活跃,避免无效执行
            ensureActive()
            delay(1000)
            emit(totalSeconds - 1 - it)
        }
    }
        .conflate() // 合并快速连续的发射,保证时间精度
}
关键改进点
  • 移除多余的 coroutineScope 阻塞:原代码中 toggleTime 内的 coroutineScope 会让函数挂起直到倒计时结束,导致后续重启调用被延迟。现在直接用当前作用域启动协程,函数调用后立即返回。
  • 用 cancelAndJoin() 确保旧任务完全终止:相比单纯 cancel(),cancelAndJoin() 会挂起直到旧协程彻底结束,避免新旧任务同时发射数据到 _countDown。
  • 重写 initTimer 为 flow 构建器:放弃序列转Flow的方式,直接用 flow 构建器更直观,且通过 ensureActive() 在每次延迟前检查协程活性,确保取消后立即停止发射。
  • 优化 onCompletion 逻辑:仅在正常结束或取消时重置倒计时为0,避免异常场景下的不必要操作。
测试代码调整(可选)

原测试中循环等待的逻辑可以简化,用 runTest 的时间控制更精准:

class CountDownTimerUseCaseTest {

    private val countDownTimerUseCase = CountDownTimerUseCase()

    @Test
    fun `重启倒计时后应重新初始化计时`() = runTest {
        countDownTimerUseCase.countDown.test {
            // 初始值
            assertEquals(0, awaitItem())

            // 启动第一次倒计时
            countDownTimerUseCase.toggleTime(3)
            assertEquals(3, awaitItem())
            advanceTimeBy(1000)
            assertEquals(2, awaitItem())

            // 中途重启
            countDownTimerUseCase.toggleTime(3)
            assertEquals(3, awaitItem())
            advanceTimeBy(1000)
            assertEquals(2, awaitItem())
            advanceTimeBy(1000)
            assertEquals(1, awaitItem())
            advanceTimeBy(1000)
            assertEquals(0, awaitItem())
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 22:35:18