AlarmManager异常:设置多餐饮时段时重复推送所有通知求助
问题分析与解决方案
核心问题定位
你的问题根源在于Flow的collect持续监听导致重复设置闹钟,以及未正确处理已过时间的闹钟逻辑,具体如下:
- 调用
scheduleMealAlarm时使用collect持续监听meal数据变化,只要Repository中任何meal数据更新(比如设置午餐时),所有已启动的collect协程都会重新执行设置闹钟的逻辑,导致旧闹钟被重复设置,甚至触发已过时间的闹钟。 - 未判断设置时间是否早于当前时间,若当前时间已过设置点,
setRepeating会立即触发一次闹钟,后续叠加到正常触发时间点导致批量通知。
修复方案
1. 替换collect为单次数据获取
设置闹钟只需要当前的meal配置,不需要持续监听变化,用first()替代collect,获取一次数据后结束协程,避免重复设置。
2. 处理已过时间的闹钟
判断设置时间是否早于当前时间,若是则将日期加一天,避免立即触发。
3. 确保PendingIntent匹配一致性
在cancelMealAlarm中保持Intent与scheduleMealAlarm一致(添加mealName extra),确保取消闹钟时能精准匹配。
修复后的代码
CCAlarmManager 修复版
class CCAlarmManager @Inject constructor( private val alarmManager: AlarmManager, private val mealTimeScreenRepositoryImpl: MealTimeScreenRepositoryImpl, private val context: Context ) { private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) fun scheduleMealAlarm(name: String) { coroutineScope.launch { // 仅获取当前最新的meal数据,而非持续监听 val meal = mealTimeScreenRepositoryImpl.getMealTimeByName(name).firstOrNull() ?: return@launch val intent = Intent(context, CCAlarmReceiver::class.java).apply { putExtra("mealName", meal.name) } val pendingIntent = PendingIntent.getBroadcast( context, meal.id, // 确保meal.id是唯一值(如1=早餐,2=午餐等) intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) // 先取消旧闹钟 alarmManager.cancel(pendingIntent) val hours = meal.time.split(":")[0].take(2).toInt() val minutes = meal.time.split(":")[1].take(2).toInt() val calendar = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, hours) set(Calendar.MINUTE, minutes) set(Calendar.SECOND, 0) set(Calendar.MILLISECOND, 0) // 若设置时间早于当前时间,日期加一天 if (timeInMillis < System.currentTimeMillis()) { add(Calendar.DAY_OF_YEAR, 1) } } // 建议使用setExactAndAllowWhileIdle替代setRepeating,适配Android 6+的省电策略 alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent ) // 若需要重复闹钟,可在BroadcastReceiver中重新设置下一次闹钟(因为setRepeating在Android 6+后精度无法保证) } } fun cancelMealAlarm(name: String) { coroutineScope.launch { val meal = mealTimeScreenRepositoryImpl.getMealTimeByName(name).firstOrNull() ?: return@launch val intent = Intent(context, CCAlarmReceiver::class.java).apply { putExtra("mealName", meal.name) // 保持与设置时的Intent一致 } val pendingIntent = PendingIntent.getBroadcast( context, meal.id, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_NO_CREATE // 使用FLAG_NO_CREATE避免创建新的PendingIntent ) pendingIntent?.let { alarmManager.cancel(it) } } } }
CCAlarmReceiver 优化版(支持重复闹钟)
如果需要重复每天的闹钟,建议在onReceive中重新设置下一天的闹钟,因为setRepeating在Android 6及以上版本的省电模式下精度会下降:
class CCAlarmReceiver: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { val mealName = intent?.getStringExtra("mealName") ?: return // 发送通知 val notification = NotificationCompat.Builder(context, "cc_notifications") .setSmallIcon(R.drawable.logo) .setContentTitle("Eating time") .setContentText("Time to eat $mealName") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .build() val notificationManager = context.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager // 使用唯一的通知ID,建议直接用meal的id而非hashCode,避免冲突 val notificationId = when(mealName) { "breakfast" -> 1 "lunch" -> 2 "dinner" -> 3 "snack" -> 4 else -> mealName.hashCode() } notificationManager.notify(notificationId, notification) // 重新设置下一天的闹钟(实现重复提醒) val ccAlarmManager = // 这里需要获取CCAlarmManager实例,可通过DI或其他方式 ccAlarmManager.scheduleMealAlarm(mealName) } }
额外注意事项
- 确保
meal.id是每个时段的唯一整数(比如早餐=1,午餐=2等),避免PendingIntent被覆盖。 - Android 12及以上版本需要在Manifest中申请
POST_NOTIFICATIONS权限,否则通知无法弹出。 - 对于重复闹钟,
setExactAndAllowWhileIdle比setRepeating更可靠,因为后者在省电模式下会被系统延迟或忽略。
内容的提问来源于stack exchange,提问作者Егор Сауткин
相关产品推荐
相关产品推荐

