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

Android Fragment中如何实现正常的倒计时定时器?

解决Fragment中倒计时Flow重复发射的问题

问题描述

我尝试在Fragment中实现一个简单的倒计时定时器(不使用ViewModel或用例),但遇到Flow随机多次发射值的问题,并非每秒仅发射一次。以下是我的代码:

定时器Flow实现

private fun startTimer(totalSeconds: Int): Flow<Int> =
    (totalSeconds - 1 downTo 0)
        .asFlow() 
        .onEach { delay(1000) } 
        .onStart { emit(totalSeconds) } 
        .conflate() 
        .transform { remainingSeconds: Int ->
            emit(remainingSeconds)
        }

Fragment的onCreate代码

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        TestTimerTheme {
            val scope = rememberCoroutineScope()
            lifecycleScope.launchWhenResumed {
                scope.launch {
                    startTimer(60)
                        .collect {
                            Log.d("MY_TAG", "sec = $it") // 每秒被调用多次
                            mutableState.emit(it)
                        }
                }
            }
            val state = mutableState.collectAsState()
            CustomTimer(state.value)
        }
    }
}

修复步骤

1. 修正定时器Flow的逻辑与冗余操作

你的Flow存在两个核心问题:

  • onStart 放置顺序错误,且原序列的执行逻辑会导致发射时机混乱;
  • conflate() 和空的transform()完全冗余,反而可能引发并发问题。

修改后的Flow实现(两种可选方式):

// 方式1:用flow builder实现更直观的逻辑
private fun startTimer(totalSeconds: Int): Flow<Int> = flow {
    emit(totalSeconds) // 先发射初始值
    repeat(totalSeconds) {
        delay(1000)
        emit(totalSeconds - it - 1) // 每秒发射递减的数值
    }
}

// 方式2:调整原序列的执行顺序
private fun startTimer(totalSeconds: Int): Flow<Int> =
    flowOf(totalSeconds)
        .concatWith((totalSeconds - 1 downTo 0).asFlow().onEach { delay(1000) })

2. 避免协程重复启动

原代码中setContent块内的协程会随Compose重组重复启动,导致多个协程同时收集Flow,这是重复发射的主要原因。用LaunchedEffect确保协程仅启动一次:

修改后的Fragment代码:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        TestTimerTheme {
            // 用mutableStateOf管理UI状态,更适配Compose场景
            val remainingSeconds = remember { mutableStateOf(60) }
            
            // LaunchedEffect(Unit)确保协程仅在首次组合时启动
            LaunchedEffect(Unit) {
                startTimer(60).collect {
                    Log.d("MY_TAG", "sec = $it")
                    remainingSeconds.value = it
                }
            }
            
            CustomTimer(remainingSeconds.value)
        }
    }
}

3. 移除不必要的嵌套协程

原代码同时使用lifecycleScope.launchWhenResumed和rememberCoroutineScope()嵌套启动协程属于冗余操作,LaunchedEffect已经在正确的协程上下文中执行,无需额外嵌套。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 06:05:19