Перейти к содержимому
Все записи

Надёжные напоминания на Android в 2026 году: WorkManager, точные будильники и новые правила батареи

Как в 2026 году выпускать напоминания на Android, которые действительно срабатывают — WorkManager против AlarmManager, SCHEDULE_EXACT_ALARM, POST_NOTIFICATIONS и причуды производителей, которые всё ещё кусаются.

MFKAPPS 7 мин чтения

Приложение-напоминалка на Android работает, только если напоминание появляется. Звучит очевидно — пока вы не выпустите такое приложение. Тогда вы обнаруживаете режим doze, корзины ожидания приложений (app standby buckets), оптимизаторы батареи от производителей, разрешение SCHEDULE_EXACT_ALARM, разрешение времени выполнения POST_NOTIFICATIONS, ограничения на службы переднего плана и тот прелестный факт, что Samsung и Xiaomi молча убьют вашу запланированную работу ради «экономии батареи», как бы правильно вы её ни написали.

Я сделал Hydrame — приложение для отслеживания воды, всё ценностное предложение которого сводится к «напомни мне в нужный момент, мягко». Я потратил больше времени, чем хотел бы признать, заставляя стек напоминаний Android быть надёжным. Этот пост — версия 2026 года того сценария, который наконец работает.

Проблема напоминаний в одном абзаце

Android агрессивен в уничтожении фоновой работы, и так и должно быть — если этого не контролировать, каждое приложение будило бы радиомодуль каждые несколько минут, и ваша батарея умирала бы к обеду. Для честных приложений-напоминалок результат — это нагромождение перекрывающихся API, каждый из которых ведёт себя немного по-разному в разных версиях и оболочках производителей. Задача в том, чтобы выбрать правильный примитив под ваш уровень «должно быть точно» и смириться с тем, что остальное — это тестирование на реальных устройствах.

Два примитива, которые имеют значение в 2026 году:

  • WorkManager — для неточной, откладываемой работы. Периодические проверки, сценарии вроде «напомни мне когда-нибудь в течение следующего часа». Учитывает батарею, объединяется, работает даже после перезагрузок.
  • AlarmManager.setExactAndAllowWhileIdle — для точных, видимых пользователю уведомлений в конкретный момент. Дорогой. Используйте экономно и только тогда, когда «промах на 15 минут» сделал бы функцию сломанной.

Если ваше напоминание должно сработать ровно в 19:30, вам нужен точный будильник. Если «где-то около 19:30» подходит, используйте WorkManager. Большинство приложений по умолчанию выбирают неправильный.

Три разрешения, которые нужно настроить правильно

В 2026 году три разрешения времени выполнения/установки решают, увидит ли пользователь напоминание вообще:

  1. POST_NOTIFICATIONS (время выполнения, Android 13+). Без него ваше уведомление буквально не появляется. Запрашивайте в правильный момент, а не при первом запуске.
  2. SCHEDULE_EXACT_ALARM (Android 12+, ограничено в 13+). Требуется для setExactAndAllowWhileIdle. На Android 14+ оно предоставляется только после того, как пользователь явно включит его в системных настройках; приложение обязано отправить пользователя туда.
  3. USE_EXACT_ALARM (Android 13+, специальное использование). Для приложений, где точные будильники — это ядро (будильники, напоминания календаря, гидратация). Play требует обоснования.

Манифест Hydrame выглядит так:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

RECEIVE_BOOT_COMPLETED — то, что большинство забывает: без него ваши запланированные будильники исчезают при следующей перезагрузке. Перепланируйте из BroadcastReceiver по ACTION_BOOT_COMPLETED.

Запрос POST_NOTIFICATIONS в правильный момент

Не запрашивайте при первом запуске. Процент согласия резко выше, если вы спрашиваете в момент, соответствующий ценности: ровно тогда, когда пользователь включает своё первое напоминание. Реальный поток:

@Composable
fun EnableReminderButton(onEnabled: () -> Unit) {
    val context = LocalContext.current
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
    ) { granted ->
        if (granted) onEnabled()
    }

    FilledButton(onClick = {
        if (Build.VERSION.SDK_INT >= 33) {
            val state = ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.POST_NOTIFICATIONS,
            )
            if (state == PackageManager.PERMISSION_GRANTED) onEnabled()
            else launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
        } else {
            onEnabled()
        }
    }) { Text("Enable reminders") }
}

Ещё две вещи, которые я делаю всегда:

  • Обоснование второго шанса. Если пользователь отказал, не спрашивайте снова сразу. В следующий раз, когда он попытается включить напоминание, покажите небольшое встроенное сообщение с кнопкой, которая открывает экран настроек приложения, чтобы он мог переключить его сам.
  • Определяйте состояние «отказано с «больше не спрашивать»», запоминая последний отказ в DataStore — shouldShowRequestPermissionRationale сам по себе для этого ненадёжен.

Неточно: периодическая работа WorkManager

Для напоминания, где «когда-нибудь утром» приемлемо, WorkManager — скучный правильный ответ:

class DailyHydrationCheckWorker(
    appContext: Context,
    params: WorkerParameters,
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        scheduleTodayReminders()
        return Result.success()
    }
}

fun scheduleDailyHydrationCheck(context: Context) {
    val request = PeriodicWorkRequestBuilder<DailyHydrationCheckWorker>(
        repeatInterval = 1, repeatIntervalTimeUnit = TimeUnit.DAYS,
    )
        .setConstraints(
            Constraints.Builder()
                .setRequiresBatteryNotLow(true)
                .build(),
        )
        .setInitialDelay(secondsUntil(6, 0).toLong(), TimeUnit.SECONDS)
        .build()

    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "daily_hydration_check",
        ExistingPeriodicWorkPolicy.UPDATE,
        request,
    )
}

Несколько моментов, на которых люди спотыкаются:

  • Минимальный период 15 минут. PeriodicWorkRequest не будет запускаться чаще, чем раз в 15 минут, что бы вы ни просили. Для планирования короче 15 минут используйте одноразовую работу, которая связывает сама себя в цепочку.
  • Нет точного тайминга. WorkManager планирует внутри окна. Если вам нужно «ровно в 8:00:00», это неправильный примитив.
  • Battery-not-low дружелюбно к вашим пользователям и к системе; включайте, если нет причины не делать этого.

Точно: AlarmManager.setExactAndAllowWhileIdle

Когда напоминание должно сработать в конкретный момент — например, пользователь сказал «напомни мне в 19:30» и имеет это в виду всерьёз — вам нужен точный будильник. Шаблон:

fun scheduleExactReminder(
    context: Context,
    triggerAtMillis: Long,
    requestCode: Int,
) {
    val am = context.getSystemService(AlarmManager::class.java)

    if (Build.VERSION.SDK_INT >= 31 && !am.canScheduleExactAlarms()) {
        // Send the user to the system page to grant the permission.
        context.startActivity(
            Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
                .setData(Uri.parse("package:${context.packageName}"))
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
        )
        return
    }

    val intent = Intent(context, ReminderReceiver::class.java)
        .putExtra("requestCode", requestCode)
    val pending = PendingIntent.getBroadcast(
        context, requestCode, intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
    )

    am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pending)
}

setExactAndAllowWhileIdle — та, что уважает doze. Используйте setAlarmClock вместо неё только если ваше приложение действительно будильник — оно показывает чип следующего будильника в системном интерфейсе и получает наивысший приоритет.

Приёмник + служба переднего плана для тяжёлого момента

Когда будильник срабатывает, у вас есть несколько секунд бодрствования, чтобы отправить уведомление. Для простого напоминания «динь!» этого достаточно:

class ReminderReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Post the notification synchronously.
        val nm = context.getSystemService(NotificationManager::class.java)
        nm.notify(
            intent.getIntExtra("requestCode", 0),
            buildHydrationNotification(context),
        )
        // Re-schedule the next one.
        scheduleNext(context)
    }
}

Если вам нужно выполнить настоящую работу по будильнику (синхронизация, загрузка), немедленно повышайте до службы переднего плана. Не пытайтесь хитрить — система отзовёт вас через мгновение, если вы застрянете в приёмнике.

Перезагрузки и смена часового пояса

Два события молча ломают наивную реализацию:

  • Перезагрузка. Будильники стираются. Слушайте ACTION_BOOT_COMPLETED и перепланируйте из вашего сохранённого состояния.
  • Смена часового пояса. Напоминание на 7 утра не должно срабатывать в 7 по местному времени до переезда пользователя и в 7 по времени Стамбула после. Слушайте ACTION_TIMEZONE_CHANGED и пересчитывайте.

Один приёмник обрабатывает оба:

<receiver android:name=".BootAndTimezoneReceiver" android:exported="false">
  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED"/>
    <action android:name="android.intent.action.TIMEZONE_CHANGED"/>
    <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
  </intent-filter>
</receiver>

MY_PACKAGE_REPLACED — то, что срабатывает при обновлениях приложения: ваш код свежий, но ваши будильники исчезли. Перепланируйте и там.

Оптимизаторы производителей — тихие убийцы

В 2026 году худший из оставшихся нарушителей — всё ещё список Samsung «Спящие приложения», а сразу за ним автозапуск и энергосбережение Xiaomi. Две практические защиты:

  1. Диагностический баннер внутри приложения. Если уведомления не срабатывали в течение 24 часов, а у пользователя включены напоминания, покажите небольшой баннер с диплинком «Исправить это» на нужную страницу настроек производителя. Stack Overflow и проект Don’t Kill My App поддерживают эти диплинки в актуальном состоянии; кэшируйте их локально.
  2. Не запрашивайте белый список батареи с рвением. Это пугает пользователей, и Play смотрит на это косо. Приберегите это для пользователя, который уже видел пропущенное напоминание.

Схема, которая в итоге сработала

После трёх итераций над Hydrame архитектура, которая наконец показалась надёжной:

  • Одна периодическая задача WorkManager запускается каждую ночь. Она читает расписание пользователя и вставляет точные будильники на следующие 24 часа.
  • Каждый точный будильник отправляет уведомление через BroadcastReceiver. Приёмник глуп — никаких обращений к БД, никакой сети.
  • Приёмники перезагрузки + смены часового пояса + обновления приложения вручную запускают ту же ночную задачу, чтобы воссоздать расписание.
  • Метрика пульса (метка времени последнего успешного уведомления, хранимая в DataStore) питает баннер внутри приложения «ваши напоминания могут быть убиты вашим телефоном».

Это больше проводки, чем я написал бы, если бы доверился своему первому порыву («просто используй WorkManager»). Но каждый кусок существует потому, что без него что-то молча ломалось.

Тестирование на реальных телефонах, снова

Повторю ту же строчку из каждого поста об Android, который пишу: надёжные напоминания нельзя протестировать только на эмуляторе. Возьмите дешёвый Samsung, дешёвый Xiaomi и Pixel. Поставьте напоминание через 30 минут. Заблокируйте телефон и оставьте его на столе. Вернитесь и посмотрите, что сработало, а что нет. Этот единственный эксперимент стоит больше, чем любой пост в блоге.

Включая этот. Идите и протестируйте.

// По теме

Ещё из журнала

MFKAPPS 4 мин чтения

Правильные действия в уведомлениях Android: отметить приём дозы без открытия приложения

Практическое руководство по кнопкам действий в уведомлениях Android — PendingIntent, ограничение activity-трамплина, goAsync() и безопасное обновление состояния Room.

#android #engineering #notifications
MFKAPPS 4 мин чтения

Как выстроить режим приёма лекарств, которого вы действительно будете придерживаться

Практическое руководство по соблюдению режима приёма лекарств — почему люди пропускают дозы, как на самом деле работает время напоминаний и как выстроить режим, который переживёт плохую неделю.

#reminders #ux #notifications
MFKAPPS 4 мин чтения

Привычка пить воду, которая не держится на силе воли

Большинство советов про питьевой режим сводятся к «пей больше воды» в надежде, что дальше сработает сила воли. Вот привычка, построенная на подсказках и мягких напоминаниях — вместо этого.

#mobile #ux #reminders