Mobile Development9 min read
Flutter Exact Alarm Scheduling on Android
Build reliable alarms in Flutter on modern Android: exact alarm permissions, Doze-safe scheduling, reboot recovery, and fallbacks from a real alarm app.
- #flutter
- #android
- #alarm-manager
- #exact-alarms
- #background-tasks

Most alarm apps fail in the most embarrassing way possible: silently. The user sets an alarm for 6:00 AM, the phone sleeps through it, and the developer gets a one-star review. I learned this building Rise Baby, a smart alarm app on the Play Store where you must complete wake-up challenges, like math problems, before the alarm lets you dismiss it. An alarm that fires at 6:07 instead of 6:00, or not at all, makes the whole concept worthless, so reliability was the entire product.
Getting this right on modern Android is harder than it looks. Since Android 6, Doze mode and App Standby have been quietly killing background work to save battery. Since Android 12, exact alarms became a special permission the user must explicitly grant, and on Android 14 that permission is denied by default for most apps. Every Android release has added another gate between your code and the user's wake-up time.
This article walks through the full pattern I settled on: requesting the exact-alarm permission correctly, scheduling with AlarmManager so the alarm survives Doze, recovering alarms after a reboot, waking the screen, and falling back gracefully when the user says no. It uses plain MethodChannels rather than a plugin, so you can see every moving part.
Why your alarm fired late, or never
The naive approach is a Future.delayed or a Timer that waits until the alarm time. It works beautifully while you are debugging, because the phone is awake and your app is in the foreground. Ship it, and the phone enters Doze: the system defers your timer callbacks to the next maintenance window, which can be hours away.
Doze is only the first wall. App Standby buckets restrict how often the system lets your app run based on how recently the user opened it. Battery-saver modes and aggressive OEM customizations add more layers. Any scheduling primitive that lives inside the Dart VM, including Timer, Future.delayed, and Dart isolates, dies with the app process. An alarm needs to survive the process being killed, the phone rebooting, and Doze. That means the schedule must live in the OS layer: AlarmManager.
Flutter notification plugins can schedule exact notifications, which is fine for reminders. But a true alarm app needs more: waking the screen, playing audio until dismissed, and a guaranteed firing time even under Doze. For Rise Baby, I went straight to AlarmManager through a platform channel.
The permission matrix: Android 12 through 14
Exact alarms went from a plain manifest permission to a special permission the user must grant. Here is what changed, version by version:
| Android version | What changed | What to do |
|---|---|---|
| 12 (API 31) | SCHEDULE_EXACT_ALARM introduced; apps targeting 31 and later must declare it and get user approval | Check AlarmManager.canScheduleExactAlarms() before scheduling |
| 13 (API 33) | POST_NOTIFICATIONS runtime permission required for apps targeting 33 and later | Request it with the normal runtime permission flow |
| 14 (API 34) | Exact-alarm permission denied by default for most apps targeting 34 and later; USE_FULL_SCREEN_INTENT permission added | Send users to Settings to enable alarms; declare the full-screen intent permission to wake the screen |
Declaring SCHEDULE_EXACT_ALARM in the manifest is no longer enough. Without the user flipping the switch in Settings, your exact-alarm calls silently behave like inexact ones.
Step 1: declare what you need in the manifest
Add these permissions to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
The boot permission is not optional for an alarm app: Android wipes every scheduled alarm on reboot, so you must reschedule them, which is covered in step 4.
Step 2: ask the user at the right moment
Never ask for the exact-alarm permission during onboarding, before the user understands why you need it. Ask the first time they create an alarm, with one sentence of context. The check-and-redirect flow lives behind a MethodChannel:
static const _channel = MethodChannel('com.rise.alarm/alarms');
/// Returns true when the app may schedule exact alarms.
Future<bool> canScheduleExactAlarms() async {
if (!Platform.isAndroid) return false;
return await _channel.invokeMethod<bool>('canScheduleExactAlarms') ?? false;
}
/// Opens the system screen where the user grants exact-alarm access.
Future<void> openExactAlarmSettings() async {
if (!Platform.isAndroid) return;
await _channel.invokeMethod('openExactAlarmSettings');
}
And on the Kotlin side, inside MainActivity:
private const val CHANNEL = "com.rise.alarm/alarms"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
when (call.method) {
"canScheduleExactAlarms" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
result.success(alarmManager.canScheduleExactAlarms())
} else {
result.success(true)
}
}
"openExactAlarmSettings" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
data = Uri.parse("package:" + packageName)
})
}
result.success(null)
}
else -> result.notImplemented()
}
}
}
The UX rule I follow: when canScheduleExactAlarms returns false, show a short explainer, like "Alarms need exact timing permission to ring on time," with a button that opens the settings screen. Do not loop the request; the user either grants it or they do not, and you need a fallback either way.
Step 3: schedule with setExactAndAllowWhileIdle
The alarm itself is scheduled natively. Build a PendingIntent targeting a BroadcastReceiver, then hand it to AlarmManager:
fun scheduleAlarm(context: Context, triggerAtMillis: Long, alarmId: Int) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, AlarmReceiver::class.java).apply {
putExtra("alarm_id", alarmId)
}
val pendingIntent = PendingIntent.getBroadcast(
context, alarmId, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent
)
}
setExactAndAllowWhileIdle is the critical API: unlike setExact, it fires even while the device is in Doze. The system throttles how often these alarms can fire in low-power idle mode, so this is the right tool for user-set alarms, not for high-frequency polling.
Keep the Dart side and the native side in sync by persisting every alarm's ID and trigger time in local storage before calling the native schedule method. That record is what the boot receiver reads to reschedule after a reboot, what the UI lists, and what you debug when a user reports a missed alarm.
Step 4: survive a reboot
Register a receiver for BOOT_COMPLETED in the manifest and point it at a BroadcastReceiver that reads your stored alarms and reschedules each future one:
<receiver
android:name=".AlarmBootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
The receiver must be fast and synchronous: read the stored alarm list, reschedule the ones still in the future, and exit. Do not start long work here. Test this on a real device: schedule an alarm, reboot, and confirm the stored record is rescheduled. Emulators often hide boot quirks that real OEM skins expose.
Step 5: actually wake the screen
An alarm that fires but does not wake the display is just a log line. The standard approach is a high-priority notification with a full-screen intent pointing at your alarm Activity. Build the notification channel with IMPORTANCE_HIGH, attach the full-screen intent, and keep USE_FULL_SCREEN_INTENT declared in the manifest, since Android 14 requires it. When the phone is locked and the screen is off, the full-screen intent launches your Activity over the lock screen; when the phone is already in use, it degrades to a heads-up notification.
One more gate: on Android 13 and later you need POST_NOTIFICATIONS granted before any of this works, so request it as part of the alarm-creation flow too.
Step 6: a graceful fallback when exact alarms are denied
Some users will deny the exact-alarm permission, and on some devices it will never be granted. Do not leave those users with nothing. The honest fallback is WorkManager with a OneTimeWorkRequest and an initial delay: it survives process death and reboots, but it is inexact, and the system can delay it. Present it truthfully in the UI as approximate reminders rather than an alarm, and keep gently nudging toward the settings screen. A degraded mode that sets correct expectations beats a fake alarm every time.
For a related angle on background scheduling that respects battery life, see Flutter Geofencing Without Battery Drain. If your alarms need server coordination, Flutter Push Notifications with Firebase covers the notification side in depth.
What I learned shipping Rise Baby
Building Rise Baby, a smart alarm app where completing wake-up challenges is the only way to dismiss the alarm, forced every one of these decisions into production code. The app has no backend: the reliable background alarm scheduling system is built from background services and local notifications entirely on-device.
Three lessons from that experience that I would repeat:
- Persist the alarm schedule in local storage first, schedule natively second. The storage record is your source of truth for reboots, for the alarm list in the UI, and for debugging missed-alarm reports.
- Ask for the exact-alarm permission in context, the moment the user creates their first alarm, not at onboarding. Grant rates were noticeably better.
- Test on physical devices from at least two OEMs. The stock Android emulator told me everything worked; a device with aggressive battery optimization taught me otherwise. When users report missed alarms, battery optimization settings are the first thing to check, and some OEMs add a separate manual auto-start permission.
The pattern that came out of this has held up in production because every failure mode got a dedicated answer: Doze got setExactAndAllowWhileIdle, reboots got the boot receiver, and denied permissions got the WorkManager fallback.
Wrapping up
Reliable alarms in Flutter are a native-integration problem wearing a Dart costume. The recipe: declare and request SCHEDULE_EXACT_ALARM, schedule through setExactAndAllowWhileIdle, add a boot receiver that reschedules from local storage, wake the screen with a full-screen intent, and keep an honest fallback for denied permissions. Get those five right and your alarms ring at 6:00, not 6:07.
If you are building a Flutter app with tricky native integrations, like alarms, geofencing, or background audio, Get in touch and let's talk about your project.
Frequently asked questions
- Why does my Flutter alarm fire late on Android?
- Because of Doze mode and App Standby. Any Timer or Future.delayed inside the Dart VM gets deferred when the phone sleeps, so the schedule must live in the OS layer using AlarmManager.
- How do I request the exact alarm permission in Flutter?
- Check AlarmManager.canScheduleExactAlarms() through a MethodChannel on Android 12 and later, and if it returns false, send the user to Settings with the ACTION_REQUEST_SCHEDULE_EXACT_ALARM intent.
- Do scheduled alarms survive a phone reboot?
- No. Android clears all scheduled alarms on reboot, so you need a RECEIVE_BOOT_COMPLETED receiver that reschedules every future alarm from your local storage.


