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

Kotlin/Android中如何运行多计时器并每秒自动更新UI?

解决放置游戏多计时器UI不实时刷新问题

问题场景与现状

我要实现放置游戏的多计时器功能:

  • 每次在本地Room DB创建新数据对象时,生成一个从指定数值(比如100)开始的独立倒计时器
  • 倒计时结束时,自动触发DB操作(比如给对应字段+1)并更新UI,无需用户交互,不管当前在哪个页面
  • 计时器要能在后台运行,切回应用时自动同步状态

使用WorkManager尝试实现后,遇到核心问题:DB里的计时器数据已更新,但只有新增任务时UI才会刷新,单个计时器的更新无法触发UI变化。

现有代码示例

JobList Composable

@Composable
fun JobList() {
    val jobViewModel: JobViewModel = hiltViewModel()
    val jobList = jobViewModel.getAllJobs()
    val jobValues = jobList.collectAsState(emptyList())

    Column {
        jobValues.value.forEach { value ->
            Button(onClick = { deleteJob(jobViewModel, value) }) {
                Text("Delete me")
            }
            val jobTimer = jobViewModel.getJobTimerDisplay(value.jobId)
            val jobTimerValue = jobTimer.collectAsState("")
            JobTimer(totalTime = jobTimerValue.value)
            Text(text = "Job Level: ${value.jobLevel} : ${value.name} : Stealth: ${value.stealthRequirement}, Brute: ${value.bruteRequirement}")
        }
    }
}

可正常更新的列表逻辑

val jobList = jobViewModel.getAllJobs()
val jobValues = jobList.collectAsState(emptyList())

无法刷新的计时器逻辑

val jobTimer = jobViewModel.getJobTimerDisplay(value.jobId)
val jobTimerValue = jobTimer.collectAsState("")
JobTimer(totalTime = jobTimerValue.value)

WorkManager实现

override suspend fun doWork(): Result {
    var working = true
    while (working) {
        delay(1001)
        val currentTime = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
        val jobId = inputData.getString("jobId")
        val job = jobId?.let { AppDatabase.getInstance(applicationContext).jobDao().loadById(it) }
        if (job != null) {
            AppDatabase.getInstance(applicationContext).jobDao()
                .updateJob(job.copy(jobTimer = currentTime))
        } else {
            working = false
        }

    }

    return Result.success()
}

问题根源分析

  1. Room Flow通知机制未利用好:getAllJobs()返回的Flow仅在列表增删时触发更新,单个Job字段变更时,若ViewModel未监听单个Job的Flow变化,UI无法感知。
  2. WorkManager逻辑低效且偏离需求:循环delay更新当前时间既浪费系统资源,也没有实现真正的倒计时逻辑(应该记录倒计时结束时间,而非实时更新当前时间)。
  3. Composable Flow收集方式错误:在forEach内部调用collectAsState,会导致列表重组时重复收集Flow,或无法正确触发UI重组。

解决方案

1. 优化Room DAO,确保单条数据变更能触发通知

修改DAO,新增单Job监听Flow,同时保留全量列表Flow的自动更新特性:

@Dao
interface JobDao {
    @Query("SELECT * FROM jobs")
    fun getAllJobs(): Flow<List<Job>>

    @Query("SELECT * FROM jobs WHERE jobId = :jobId")
    fun getJobById(jobId: String): Flow<Job?>

    @Update
    suspend fun updateJob(job: Job)

    // 其他增删方法
}

2. 重构ViewModel的计时器逻辑

改为监听单Job的变化,计算剩余倒计时时间,同时优化WorkManager调度逻辑:

class JobViewModel @Inject constructor(private val jobDao: JobDao) : ViewModel() {
    fun getAllJobs(): Flow<List<Job>> {
        return jobDao.getAllJobs()
    }

    // 监听单个Job,计算并返回格式化的剩余倒计时
    fun getRemainingTimeFlow(jobId: String): Flow<String> {
        return jobDao.getJobById(jobId)
            .filterNotNull()
            .map { job ->
                val currentTime = System.currentTimeMillis()
                val remainingTime = job.countdownEndTime - currentTime
                if (remainingTime <= 0) {
                    "已完成"
                } else {
                    val seconds = (remainingTime / 1000).toInt()
                    String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)
                }
            }
            .distinctUntilChanged() // 避免重复发送相同值,减少无用重组
    }

    // 创建新Job时,初始化倒计时结束时间并调度Work任务
    suspend fun startNewJob(job: Job) {
        val countdownDuration = 100 * 1000 // 对应需求的100秒倒计时
        val endTime = System.currentTimeMillis() + countdownDuration
        jobDao.insertJob(job.copy(countdownEndTime = endTime))
        scheduleCountdownWork(job.jobId, endTime)
    }

    // 调度倒计时结束任务,避免循环浪费资源
    private fun scheduleCountdownWork(jobId: String, endTime: Long) {
        val delayMillis = endTime - System.currentTimeMillis()
        if (delayMillis <= 0) {
            completeJob(jobId)
            return
        }

        val workRequest = OneTimeWorkRequestBuilder<CountdownWorker>()
            .setInitialDelay(delayMillis, TimeUnit.MILLISECONDS)
            .setInputData(workDataOf("jobId" to jobId))
            .build()

        WorkManager.getInstance().enqueue(workRequest)
    }

    // 倒计时结束后的业务逻辑:升级Job等级,可选自动开启下一轮
    suspend fun completeJob(jobId: String) {
        val job = jobDao.getJobById(jobId).firstOrNull() ?: return
        jobDao.updateJob(job.copy(
            jobLevel = job.jobLevel + 1,
            countdownEndTime = System.currentTimeMillis() + 100 * 1000
        ))
    }
}

3. 修正WorkManager实现

改为一次性任务,仅在倒计时结束时执行业务逻辑:

class CountdownWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        val jobId = inputData.getString("jobId") ?: return Result.failure()
        val jobDao = AppDatabase.getInstance(applicationContext).jobDao()
        val job = jobDao.getJobById(jobId).firstOrNull() ?: return Result.failure()

        // 执行倒计时完成逻辑
        jobDao.updateJob(job.copy(
            jobLevel = job.jobLevel + 1,
            countdownEndTime = System.currentTimeMillis() + 100 * 1000
        ))

        return Result.success()
    }
}

4. 优化Composable的UI刷新逻辑

调整Flow收集位置,确保单Job计时器变化能触发UI重组:

@Composable
fun JobList() {
    val jobViewModel: JobViewModel = hiltViewModel()
    val jobList by jobViewModel.getAllJobs().collectAsState(emptyList())

    Column {
        jobList.forEach { job ->
            Button(onClick = { /* 实现删除逻辑 */ }) {
                Text("Delete me")
            }
            // 直接收集单Job的剩余时间Flow,确保变化时触发重组
            val remainingTime by jobViewModel.getRemainingTimeFlow(job.jobId).collectAsState("")
            JobTimer(totalTime = remainingTime)
            Text(text = "Job Level: ${job.jobLevel} : ${job.name} : Stealth: ${job.stealthRequirement}, Brute: ${job.bruteRequirement}")
        }
    }
}

额外注意事项

  • 后台运行保障:WorkManager的OneTimeWorkRequest在设备开机状态下会自动执行,但若受系统电池优化限制,可考虑配合前台服务提升优先级(需申请对应权限)。
  • 避免重复任务:创建新Job时,可先取消该Job对应的旧Work任务,防止重复执行。
  • Room Flow特性:Room的Flow为冷流,仅在收集时触发查询,数据变更时自动推送新值,确保UI实时同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 07:07:55