通过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
相关产品推荐
相关产品推荐

