Как правильно использовать AlarmManager в Android для отложенных задач и напоминаний?
Проблема и её причины
Основная ошибка в вашем коде — использование метода setRepeating(). Начиная с Android 4.4 (API 19) этот метод перестал быть точным: система может объединять повторяющиеся будильники для экономии батареи. А начиная с Android 6.0 (API 23) появился режим Doze, который ещё больше ограничивает работу AlarmManager. Кроме того, setRepeating() вообще не гарантирует точного срабатывания. Для точных повторяющихся событий нужно использовать setExactAndAllowWhileIdle() и самостоятельно переустанавливать будильник после каждого срабатывания.
Правильное решение (Kotlin + BroadcastReceiver)
Вот рабочий пример для API 23+ (Android 6.0+). Он совместим с Doze-режимом и корректно обрабатывает перезагрузку.
1. Создаём BroadcastReceiver
class ReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Здесь показываем уведомление
showNotification(context, "Напоминание", "Не забудьте сделать дело!")
// Переустанавливаем будильник на следующий день
setDailyAlarm(context)
}
private fun showNotification(context: Context, title: String, message: String) {
val channelId = "reminder_channel"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Напоминания",
NotificationManager.IMPORTANCE_HIGH
)
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build()
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.notify(1, notification)
}
}
2. Функция установки будильника (в Activity или Application)
fun setDailyAlarm(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, ReminderReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Вычисляем время ближайшего срабатывания
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 9)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
// Если текущее время уже прошло 9:00, устанавливаем на завтра
if (before(Calendar.getInstance())) {
add(Calendar.DAY_OF_YEAR, 1)
}
}
// Используем setExactAndAllowWhileIdle для точности даже в Doze
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
}
3. Регистрация в манифесте
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Для Android 13+ -->
<application ...>
<receiver android:name=".ReminderReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
4. Обработка перезагрузки
Создайте второй BroadcastReceiver, который будет переустанавливать будильник после загрузки системы:
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
setDailyAlarm(context)
}
}
}
Зарегистрируйте его в манифесте аналогично, указав android.intent.action.BOOT_COMPLETED.
5. Отмена будильника
fun cancelAlarm(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, ReminderReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
pendingIntent.cancel()
}
Важные замечания
- Для Android 12+ обязательно используйте флаг
FLAG_IMMUTABLEвPendingIntent. - Для Android 13+ не забудьте запросить разрешение
POST_NOTIFICATIONSв рантайме. - Если ваше приложение работает на Android 12 и выше, добавьте в манифест
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />и проверьте наличие разрешения черезalarmManager.canScheduleExactAlarms().
Альтернативный подход: WorkManager
Если вам не нужна абсолютная точность (допустима задержка в несколько минут), рекомендую использовать WorkManager вместо AlarmManager. WorkManager гораздо проще в настройке, автоматически учитывает Doze-режим и перезагрузки устройства, а также не требует ручного управления BroadcastReceiver.
Пример с WorkManager
// В build.gradle добавьте зависимость:
// implementation "androidx.work:work-runtime-ktx:2.8.1"
class DailyReminderWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
// Показываем уведомление
showNotification(applicationContext, "Напоминание", "Пора действовать!")
return Result.success()
}
}
// Запланировать ежедневное напоминание:
val dailyRequest = PeriodicWorkRequestBuilder<DailyReminderWorker>(
24, TimeUnit.HOURS // интервал
).setInitialDelay(calculateDelayUntil(9, 0), TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"daily_reminder",
ExistingPeriodicWorkPolicy.KEEP,
dailyRequest
)
// Функция для расчёта задержки до 9:00
fun calculateDelayUntil(hour: Int, minute: Int): Long {
val now = Calendar.getInstance()
val target = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
if (before(now)) add(Calendar.DAY_OF_YEAR, 1)
}
return target.timeInMillis - now.timeInMillis
}
Преимущества
- Не нужно писать BroadcastReceiver для перезагрузки.
- Работает на всех версиях Android, начиная с API 14.
- Автоматически адаптируется к режимам энергосбережения.
Недостаток: минимальный интервал для периодической работы — 15 минут (для точных ежедневных задач это не проблема). Если вам нужно точное срабатывание в конкретное время, используйте OneTimeWorkRequest и перепланируйте