纯Kotlin实现可重启倒计时遇问题:无法终止旧任务重启
问题分析
你的倒计时重启时旧任务未停止的核心问题有两个:
toggleTime中的coroutineScope会挂起函数直到内部协程完成,导致首次调用后函数一直阻塞,后续重启调用的时机和取消逻辑被干扰。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
相关产品推荐
相关产品推荐

