Правильные действия в уведомлениях Android: отметить приём дозы без открытия приложения
Практическое руководство по кнопкам действий в уведомлениях Android — PendingIntent, ограничение activity-трамплина, goAsync() и безопасное обновление состояния Room.
Если напоминание заставляет открывать приложение, чтобы его закрыть, вы добавили шаг, который никто не просил. Весь смысл уведомления в том, что оно само и есть интерфейс — пользователь реагирует на него и идёт дальше. OldSchool, напоминание о приёме лекарств, полностью на этом держится: кнопки на экране блокировки Принято, Отложить и Пропустить должны срабатывать мгновенно, обновлять нужные данные и никогда не запускать Activity, которую пользователь не просил показывать. Вот как это устроено — и два-три места, где Android усложняет задачу больше, чем нужно.
Проблема «просто открой приложение»
Наивная версия уведомления-напоминания — это одно касание, открывающее приложение на экране, где нужно нажать кнопку. Это два лишних действия для того, что должно быть одним. Это также означает холодный запуск Activity каждый раз — на телефоне среднего класса с десятком других приложений в памяти это заметная задержка между «я увидел напоминание» и «оно отмечено выполненным».
Решение — NotificationCompat.Action: кнопки, прикреплённые прямо к уведомлению, которые срабатывают, ни разу не выводя Activity на передний план.
val takenIntent = Intent(context, DoseActionReceiver::class.java).apply {
action = ACTION_MARK_TAKEN
putExtra(EXTRA_DOSE_ID, doseId)
}
val takenPendingIntent = PendingIntent.getBroadcast(
context,
doseId.hashCode(),
takenIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle("Metformin — 500mg")
.addAction(R.drawable.ic_check, "Taken", takenPendingIntent)
.addAction(R.drawable.ic_snooze, "Snooze", snoozePendingIntent)
.addAction(R.drawable.ic_skip, "Skip", skipPendingIntent)
.build()
Три кнопки, три PendingIntent, каждый указывает на BroadcastReceiver, а не на Activity. Это различие и есть весь фокус.
Почему receiver, а не Activity
Соблазнительно направить действие через Activity — она у вас уже есть, она уже общается с базой данных. Не делайте этого. Начиная с Android 12, запуск Activity напрямую из действия уведомления (а не из основной области касания самого уведомления) ограничен как «activity-трамплин», и даже там, где это ещё работает, это означает нежелательную вспышку интерфейса, запись в переключателе задач и более медленный отклик. BroadcastReceiver ничего этого не делает — он выполняется, делает свою работу, и ничего видимого не происходит, кроме обновления уведомления.
FLAG_IMMUTABLE тоже важен: начиная с Android 12, изменяемые PendingIntent по умолчанию блокируются для всего, что явно не требует FLAG_MUTABLE (например, RemoteInput). Обычной кнопке действия изменяемость не нужна — отметьте её как immutable и двигайтесь дальше.
Реальная работа в receiver: goAsync()
Проблема с BroadcastReceiver.onReceive() в том, что от него ожидается возврат почти мгновенно — несколько миллисекунд, а не время, которое занимает запись в Room в фоновом потоке. Если вы вернётесь до завершения записи, система может убить процесс прямо посреди записи на некоторых оболочках производителей.
goAsync() корректно покупает это дополнительное время:
class DoseActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val doseId = intent.getLongExtra(EXTRA_DOSE_ID, -1)
val pendingResult = goAsync()
CoroutineScope(Dispatchers.IO).launch {
try {
doseDao.markTaken(doseId, timestamp = System.currentTimeMillis())
NotificationManagerCompat.from(context).cancel(doseId.toInt())
} finally {
pendingResult.finish()
}
}
}
}
goAsync() даёт вам примерно 10 секунд продлённого времени жизни — достаточно для записи в Room, но недостаточно, чтобы оправдать foreground-сервис. Всегда вызывайте pendingResult.finish() в блоке finally; забыть об этом на пути исключения означает, что система будет держать wakelock, который не может освободить, и в итоге пометит ваш receiver как ведущий себя некорректно.
Держать уведомление и базу данных в согласии
Отметить дозу как «принятую» — это не просто строка в базе данных: само уведомление должно исчезнуть или измениться, иначе пользователь нажмёт на него снова и удивится, почему ничего не происходит. Вызов NotificationManagerCompat.cancel() после записи — это минимум; для Отложить я перепланирую через тот же путь AlarmManager/WorkManager, который использовало исходное напоминание (см. статью о WorkManager и точных будильниках для планировочной половины этого), а не изобретаю второй конвейер уведомлений. Одно напоминание, один путь кода — независимо от того, сработало оно в первый раз или в третий.
Другая распространённая ошибка — обновлять состояние в памяти, за которым никто не наблюдает. Если представление дня опирается на Flow из Room, записи receiver достаточно — интерфейс перекомпонуется в следующий раз, когда станет видимым, без единой строчки явного кода синхронизации. Если нет, вы получите устаревшие данные в тот момент, когда пользователь вернётся в приложение, и в итоге будете отлаживать состояние гонки, которое на самом деле — просто отсутствующий наблюдатель.
Что это даёт
Измеримая разница на бумаге не выглядит впечатляюще — на один экран меньше, на одно касание меньше — но она меняет то, как ощущается напоминание. Уведомление, которое разрешается само за меньше чем секунду, прямо там, где появилось, воспринимается как уважение к тем двум секундам, которые вы ему дали. То, что требует холодного запуска приложения, воспринимается как трение — а трение превращает напоминание в то, что смахивают по привычке, вместо того, чем пользуются.
Если вы строите любое уведомление типа «отреагируй прямо сейчас» — напоминание, подталкивание к задаче, подтверждение доставки — паттерн работает независимо от предметной области: направляйте действия через BroadcastReceiver, продлевайте окно с помощью goAsync(), пишите через существующий слой данных и давайте реактивному интерфейсу подхватить изменение вместо того, чтобы проталкивать его вручную. Это немного «сантехники» ради функции, которую пользователи замечают только тогда, когда её нет.
// По теме
Ещё из журнала
Надёжные напоминания на Android в 2026 году: WorkManager, точные будильники и новые правила батареи
Как в 2026 году выпускать напоминания на Android, которые действительно срабатывают — WorkManager против AlarmManager, SCHEDULE_EXACT_ALARM, POST_NOTIFICATIONS и причуды производителей, которые всё ещё кусаются.
Как выстроить режим приёма лекарств, которого вы действительно будете придерживаться
Практическое руководство по соблюдению режима приёма лекарств — почему люди пропускают дозы, как на самом деле работает время напоминаний и как выстроить режим, который переживёт плохую неделю.
Привычка пить воду, которая не держится на силе воли
Большинство советов про питьевой режим сводятся к «пей больше воды» в надежде, что дальше сработает сила воли. Вот привычка, построенная на подсказках и мягких напоминаниях — вместо этого.