Saltar al contenido
Todas las entradas

Recordatorios fiables en Android en 2026: WorkManager, alarmas exactas y las nuevas reglas de batería

Cómo publicar en 2026 recordatorios en Android que de verdad se disparen — WorkManager vs AlarmManager, SCHEDULE_EXACT_ALARM, POST_NOTIFICATIONS y las rarezas de los fabricantes que todavía muerden.

MFKAPPS 8 min de lectura

Una aplicación de recordatorios en Android solo funciona si el recordatorio aparece. Suena obvio — hasta que publicas una. Entonces descubres el modo doze, los cubos de espera de aplicaciones (app standby buckets), los optimizadores de batería de los fabricantes, el permiso SCHEDULE_EXACT_ALARM, el permiso en tiempo de ejecución POST_NOTIFICATIONS, las restricciones de los servicios en primer plano, y el encantador hecho de que Samsung y Xiaomi matarán silenciosamente tu trabajo programado para «ahorrar batería», por muy correcto que lo hayas escrito.

Construí Hydrame, una aplicación de seguimiento de agua cuya propuesta de valor entera es «recuérdamelo en el momento justo, con suavidad». He dedicado más tiempo del que me gustaría admitir a forzar la pila de recordatorios de Android hacia la fiabilidad. Este artículo es la versión de 2026 del manual que por fin funciona.

El problema de los recordatorios en un párrafo

Android es agresivo a la hora de matar el trabajo en segundo plano, y debe serlo — sin control, cada aplicación despertaría la radio cada pocos minutos y tu batería moriría antes del almuerzo. El resultado para las aplicaciones de recordatorios legítimas es una pila de API superpuestas que se comportan cada una de forma ligeramente distinta según las versiones y las capas de los fabricantes. El trabajo consiste en elegir la primitiva adecuada para tu nivel de «debe ser exacto» y aceptar que el resto es probar en dispositivos reales.

Las dos primitivas que importan en 2026 son:

  • WorkManager — para trabajo inexacto y aplazable. Comprobaciones periódicas, casos de uso de «recuérdamelo en algún momento de la próxima hora». Consciente de la batería, se agrupa, se ejecuta incluso tras reinicios.
  • AlarmManager.setExactAndAllowWhileIdle — para notificaciones exactas, visibles para el usuario, en un momento concreto. La cara. Úsala con moderación y solo cuando «desviarse 15 minutos» haría que la función pareciera rota.

Si tu recordatorio tiene que dispararse exactamente a las 19:30, necesitas una alarma exacta. Si «alrededor de las 19:30» está bien, usa WorkManager. La mayoría de las aplicaciones eligen por defecto la equivocada.

Tres permisos que tienes que acertar

En 2026, tres permisos en tiempo de ejecución/instalación deciden si el usuario llega a ver un recordatorio:

  1. POST_NOTIFICATIONS (en tiempo de ejecución, Android 13+). Sin él, tu notificación literalmente no aparece. Pídelo en el momento adecuado, no en el primer arranque.
  2. SCHEDULE_EXACT_ALARM (Android 12+, restringido en 13+). Necesario para setExactAndAllowWhileIdle. En Android 14+ solo se concede después de que el usuario lo active explícitamente en los ajustes del sistema; la aplicación tiene que enviar al usuario allí.
  3. USE_EXACT_ALARM (Android 13+, uso especial). Para aplicaciones donde las alarmas exactas son el núcleo (despertadores, recordatorios de calendario, hidratación). Play pide una justificación.

El manifiesto de Hydrame se ve así:

<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 es el que la mayoría olvida — sin él, tus alarmas programadas desaparecen en el siguiente reinicio. Vuelve a programarlas desde un BroadcastReceiver en ACTION_BOOT_COMPLETED.

Pedir POST_NOTIFICATIONS en el momento adecuado

No lo pidas en el primer arranque. La tasa de aceptación es muchísimo mayor si lo pides en un momento que se corresponde con el valor: justo cuando el usuario activa su primer recordatorio. Un flujo real:

@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") }
}

Dos cosas más que siempre hago:

  • Una justificación de segunda oportunidad. Si el usuario deniega, no vuelvas a preguntar de inmediato. La próxima vez que intente activar un recordatorio, muestra un pequeño mensaje en línea con un botón que abra la pantalla de ajustes de la aplicación para que pueda activarlo él mismo.
  • Detecta el estado de denegado-con-no-volver-a-preguntar recordando el último rechazo en DataStore — shouldShowRequestPermissionRationale no es fiable por sí solo para esto.

Inexacto: trabajo periódico de WorkManager

Para un recordatorio en el que «en algún momento de la mañana» es aceptable, WorkManager es la aburrida respuesta correcta:

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,
    )
}

Algunas cosas que muerden a la gente:

  • Período mínimo de 15 minutos. Un PeriodicWorkRequest no se ejecutará más a menudo que cada 15 minutos, sin importar lo que pidas. Para programaciones de menos de 15 minutos, usa trabajo puntual que se encadena a sí mismo.
  • Sin sincronización exacta. WorkManager programa dentro de una ventana. Si necesitas «a las 8:00:00 en punto», esta es la primitiva equivocada.
  • Battery-not-low es amable con tus usuarios y con el sistema; actívala salvo que tengas una razón para no hacerlo.

Exacto: AlarmManager.setExactAndAllowWhileIdle

Cuando el recordatorio tiene que dispararse en un momento concreto — por ejemplo, un usuario dijo «recuérdamelo a las 19:30» y va en serio — necesitas una alarma exacta. El patrón:

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 es la que respeta el modo doze. Usa setAlarmClock en su lugar solo si tu aplicación es realmente un despertador — muestra el chip de la próxima alarma en la interfaz del sistema y obtiene la máxima prioridad.

Receiver + servicio en primer plano para el momento pesado

Cuando la alarma se dispara, tienes unos pocos segundos de tiempo despierto para publicar una notificación. Para un simple recordatorio de «¡ding!» eso basta:

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)
    }
}

Si necesitas hacer trabajo real en la alarma (sincronizar, descargar), promociónalo de inmediato a un servicio en primer plano. No intentes ser ingenioso — el sistema te revocará en un instante si te quedas atascado en el receiver.

Reinicios y cambios de zona horaria

Dos eventos rompen silenciosamente una implementación ingenua:

  • Reinicio. Las alarmas se borran. Escucha ACTION_BOOT_COMPLETED y vuelve a programar desde tu estado persistente.
  • Cambio de zona horaria. Un recordatorio de las 7 de la mañana no debería dispararse a las 7 hora local antes de que el usuario se mudara y a las 7 hora de Estambul después. Escucha ACTION_TIMEZONE_CHANGED y recalcula.

Un único receiver maneja ambos:

<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 es el que disparan las actualizaciones de la aplicación — tu código está fresco, pero tus alarmas han desaparecido. Vuelve a programar ahí también.

Los optimizadores de los fabricantes — los asesinos silenciosos

En 2026 el peor infractor que queda sigue siendo todavía la lista de «Aplicaciones en suspensión» de Samsung, con el autoarranque y el ahorro de batería de Xiaomi justo detrás. Dos defensas prácticas:

  1. Banner de diagnóstico dentro de la aplicación. Si no se han disparado notificaciones en 24 horas y el usuario tiene recordatorios activados, muestra un pequeño banner con un enlace profundo «Arreglar esto» a la página correcta de ajustes del fabricante. Stack Overflow y el proyecto Don’t Kill My App mantienen actualizados esos enlaces profundos; guárdalos en caché localmente.
  2. No pidas la lista blanca de batería con avidez. Asusta a los usuarios y Play lo ve con malos ojos. Resérvalo para el usuario que ya ha visto un recordatorio perdido.

El patrón que acabó funcionando

Tras tres iteraciones en Hydrame, la arquitectura que por fin se sintió fiable:

  • Un único trabajo periódico de WorkManager se ejecuta cada noche. Lee la programación del usuario e inserta las alarmas exactas de las siguientes 24 horas.
  • Cada alarma exacta publica una notificación a través de un BroadcastReceiver. El receiver es tonto — sin llamadas a la base de datos, sin red.
  • Los receivers de reinicio + cambio de zona horaria + actualización de la aplicación disparan manualmente ese mismo trabajo nocturno para recrear la programación.
  • Una métrica de latido (marca de tiempo de la última notificación exitosa, almacenada en DataStore) alimenta el banner dentro de la aplicación de «tus recordatorios pueden ser eliminados por tu teléfono».

Es más cableado del que habría escrito si hubiera confiado en mi primer instinto («usa solo WorkManager»). Pero cada pieza existe porque algo se rompió silenciosamente sin ella.

Probar en teléfonos reales, otra vez

Repetiré la misma frase de cada artículo de Android que escribo: no puedes probar recordatorios fiables solo en el emulador. Consigue un Samsung barato, un Xiaomi barato y un Pixel. Programa un recordatorio para dentro de 30 minutos. Bloquea el teléfono y déjalo sobre el escritorio. Vuelve y comprueba qué se disparó y qué no. Ese único experimento vale más que cualquier artículo de blog.

Incluido este. Ve a probarlo.