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

通过Pending Intent打开应用后CountDownTimer状态重置问题求助

解决倒计时应用从通知返回后状态丢失、计时器中断的问题

我实现了一个倒计时功能,使用CountDownTimer在ViewModel中处理计时,通过onTick更新State并通知前台Service更新通知内容。应用退到后台时前台服务正常运行,通知能正确显示剩余时间,但点击通知的PendingIntent回到应用后,ViewModel的State回到初始值,无法展示剩余时间,计时器也停止运行。尝试过修改launchMode、使用collectLatest、恢复时重建计时器(但会导致双计时器),希望能让计时器从暂停位置继续运行。

问题根源

  • 计时逻辑绑定在ViewModel中,ViewModel生命周期依附于UI组件,应用退到后台后,系统可能回收进程或销毁Activity,导致ViewModel重建,原有CountDownTimer实例丢失。
  • 前台Service仅负责更新通知,不持有计时逻辑,无法在ViewModel重建时提供当前剩余时间。
  • 点击通知返回时,新创建的ViewModel未同步Service中记录的当前计时状态。

解决方案:将计时逻辑移至前台Service,实现Service与ViewModel的状态同步

1. 修改TimerService,内置计时逻辑并通过Flow对外发送剩余时间

把原ViewModel中的CountDownTimer移到Service中,用MutableSharedFlow发送剩余时间,让ViewModel可以订阅获取实时状态:

@AndroidEntryPoint
class TimerService : Service() {
    @Inject
    lateinit var notificationM: NotificationManager
    @Inject
    lateinit var notificationB: NotificationCompat.Builder
    @Inject
    lateinit var timerUtil: TimerStringLongConverterUtils

    private var timerServiceIsRunning: Boolean = false
    private lateinit var timer: CountDownTimer
    private val _remainingTimeFlow = MutableSharedFlow<String>(replay = 1)
    val remainingTimeFlow = _remainingTimeFlow.asSharedFlow()

    private var currentRemainingTime: Long = 0

    override fun onBind(intent: Intent?): IBinder? {
        return TimerBinder()
    }

    inner class TimerBinder : Binder() {
        fun getService(): TimerService = this@TimerService
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        intent?.action.let { intentAction ->
            when (intentAction) {
                TimerServiceConstant.START.name -> {
                    if (!timerServiceIsRunning) {
                        timerServiceIsRunning = true
                        createNotificationChannel()
                        val initialTime = intent?.getLongExtra("initial_time", 0) ?: 0
                        startTimer(initialTime)
                        ServiceCompat.startForeground(
                            this,
                            TimerServiceConstant.TIMER_NOTIFICATION_ID.value,
                            notificationB.build(),
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                                ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
                            } else 0
                        )
                    }
                }
                TimerServiceConstant.CANCEL.name -> {
                    if (timerServiceIsRunning) {
                        stopTimerService()
                    }
                }
            }
        }
        return START_STICKY
    }

    private fun startTimer(time: Long) {
        currentRemainingTime = time
        timer = object : CountDownTimer(time, 1000) {
            override fun onTick(millisUntilFinished: Long) {
                currentRemainingTime = millisUntilFinished
                val remainingTimeString = timerUtil.convertTimeLongToString(millisUntilFinished / 1000)
                updateNotification(remainingTimeString)
                CoroutineScope(Dispatchers.Main).launch {
                    _remainingTimeFlow.emit(remainingTimeString)
                }
            }

            override fun onFinish() {
                timerServiceIsRunning = false
                stopForeground(STOP_FOREGROUND_REMOVE)
                stopSelf()
                notificationM.cancel(TimerServiceConstant.TIMER_NOTIFICATION_ID.value)
            }
        }
        timer.start()
    }

    private fun stopTimerService() {
        timerServiceIsRunning = false
        timer.cancel()
        stopForeground(STOP_FOREGROUND_REMOVE)
        stopSelf()
        notificationM.cancel(TimerServiceConstant.TIMER_NOTIFICATION_ID.value)
    }

    private fun updateNotification(timerTime: String) {
        notificationM.notify(
            TimerServiceConstant.TIMER_NOTIFICATION_ID.value,
            notificationB.setContentText(timerTime).build()
        )
    }

    private fun createNotificationChannel() {
        val channel = NotificationChannel(
            TimerServiceConstant.TIMER_CHANNEL_ID.name,
            TimerServiceConstant.TIMER_NOTIFICATION_CHANNEL_NAME.name,
            NotificationManager.IMPORTANCE_LOW
        )
        notificationM.createNotificationChannel(channel)
    }

    fun getCurrentRemainingTime(): Long {
        return currentRemainingTime
    }
}

2. 修改ViewModel,绑定Service并订阅剩余时间Flow

ViewModel通过ServiceConnection绑定到TimerService,订阅剩余时间Flow更新UI状态,初始化时同步当前计时状态:

@HiltViewModel
class DashboardViewModel @Inject constructor(
    private val wrap: DashboardWrapper,
    private val timerUtil: TimerStringLongConverterUtils,
    private val timerServiceHelper: TimerServiceHelper,
    private val appContext: Context
) : ViewModel() {
    private val _state = MutableStateFlow(DashboardState())
    val state = _state.asStateFlow()
    private var isServiceBound = false
    private var timerService: TimerService? = null

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            val binder = service as TimerService.TimerBinder
            timerService = binder.getService()
            isServiceBound = true
            viewModelScope.launch {
                timerService?.remainingTimeFlow?.collect { timeString ->
                    val remainingTimeLong = timerUtil.convertTimeStringToSeconds(timeString)
                    _state.update {
                        it.copy(
                            remainingTimeLong = remainingTimeLong,
                            remainingTimeString = timeString,
                            isDashboardTimerRunningForDecidingWhetherToStartOrNotStartTimerService = true
                        )
                    }
                }
            }
            timerService?.getCurrentRemainingTime()?.let { millis ->
                val timeString = timerUtil.convertTimeLongToString(millis / 1000)
                _state.update {
                    it.copy(
                        remainingTimeLong = millis / 1000,
                        remainingTimeString = timeString,
                        isDashboardTimerRunningForDecidingWhetherToStartOrNotStartTimerService = timerService?.timerServiceIsRunning == true
                    )
                }
            }
        }

        override fun onServiceDisconnected(arg0: ComponentName) {
            isServiceBound = false
            timerService = null
        }
    }

    init {
        Intent(appContext, TimerService::class.java).also { intent ->
            appContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
        }
    }

    fun habitEvents(e: DashboardHabitEvents) {
        when (e) {
            is DashboardHabitEvents.StartStopTimer -> {
                when (state.value.timerMode) {
                    TimerModeConstants.STOP -> {
                        timerServiceHelper.stopForegroundService(TimerServiceConstant.CANCEL.name)
                        _state.update {
                            it.copy(
                                isDashboardTimerRunningForDecidingWhetherToStartOrNotStartTimerService = false
                            )
                        }
                    }
                    TimerModeConstants.START -> {
                        val remainingTimeInMills = timerUtil.convertTimeStringToSeconds(state.value.remainingTimeString) * 1000
                        timerServiceHelper.triggerForegroundServiceWithInitialTime(remainingTimeInMills)
                    }
                }
            }
            else -> {}
        }
    }

    override fun onCleared() {
        super.onCleared()
        if (isServiceBound) {
            appContext.unbindService(serviceConnection)
            isServiceBound = false
        }
    }
}

3. 修改TimerServiceHelper,添加传递初始时间的方法

class TimerServiceHelper @Inject constructor(
    private val appContext: Context
) {
    private var isTimerserviceRunning: Boolean = false

    fun triggerForegroundServiceWithInitialTime(initialTime: Long) {
        Intent(appContext, TimerService::class.java).apply {
            isTimerserviceRunning = true
            this.action = TimerServiceConstant.START.name
            putExtra("initial_time", initialTime)
            appContext.startForegroundService(this)
        }
    }

    fun stopForegroundService(newAction: String) {
        if (isTimerserviceRunning) {
            Intent(appContext, TimerService::class.java).apply {
                this.action = newAction
                appContext.stopService(this)
            }
            isTimerserviceRunning = false
        }
    }
}

4. 调整PendingIntent和Activity启动模式

  • 在AndroidManifest.xml中设置MainActivity的launchMode为singleTop:
<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"
    ...>
    ...
</activity>
  • 修改PendingIntent的flag,确保Intent数据最新:
class TimerServicePendingIntentHandler(
    private val appContext: Context
) {
    fun clickPendingIntent(): PendingIntent {
        val deepLinkIntent = Intent(
            Intent.ACTION_VIEW,
            DashboardScreenRoutes.MainDashboardScreen.route.toUri(),
            appContext,
            MainActivity::class.java
        )
        return TaskStackBuilder.create(appContext).run {
            addNextIntentWithParentStack(deepLinkIntent)
            getPendingIntent(
                TimerServiceConstant.TIMER_SERVICE_REQUEST_CODE.value,
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
            )
        }
    }
}

关键说明

  • 计时逻辑移到前台Service后,不受ViewModel和Activity生命周期影响,即使应用退到后台或进程被回收,计时仍能继续。
  • 通过ServiceConnection绑定Service,ViewModel可实时获取剩余时间,确保UI状态与Service同步。
  • MutableSharedFlow的replay=1配置,确保ViewModel订阅时能立即获取最新剩余时间,解决回到应用时状态为空的问题。

内容的提问来源于stack exchange,提问作者Narayan Dutt Purohit

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 16:32:32