Practical, field-tested notes for adding scheduled local notifications to a Flutter app — the kind that fire at an exact time even when the app is closed and the phone is offline (prayer alerts, alarms, reminders).
Not a tutorial for the happy path — the package docs cover that. This is a record of what actually goes wrong, on both platforms, and how to fix it. Written while building Steps4Perks and a prayer-times app, both shipping to Android and iOS.
Scope:
flutter_local_notifications, scheduled local notifications. Push / FCM is a different problem and isn't covered.
| Android → | Gradle desugaring, manifest permissions and receivers, exact alarms, boot receivers, and the OEM battery-optimisation trap |
| iOS → | Darwin init settings, the AppDelegate wiring that fails silently, permission flow, and the 64-notification cap |
| Symptom | Platform | Go to |
|---|---|---|
| Fires when backgrounded, dies when app is swiped away | Android | Battery optimisation |
| Notifications arrive late or batched together | Android | Exact alarms |
| Build fails: "requires core library desugaring" | Android | Gradle config |
| Alarms gone after a reboot | Android | Boot receiver |
| Crash: "iOS settings must be set when targeting iOS platform" | iOS | Init settings |
Schedules fine, pendingNotificationRequests() looks right, nothing ever appears |
iOS | AppDelegate wiring |
| Some notifications never fire, no error | iOS | The 64 cap |
| Test notification doesn't show while using the app | iOS | Expected — background the app first |
Both platforms need the same three things: timezone-pinned scheduling, runtime permission, and platform-specific config you won't get an error about if you skip it.
The hard part is different on each:
- Android is hard after it works. The code is straightforward; the pain is OEM battery optimisation silently killing your alarms on real devices. Always test with the app fully killed, on the actual phone you care about.
- iOS is hard before it works. Once running it's rock solid — the OS owns
the schedule, so nothing can kill it — but the setup has a step
(
AppDelegate.swift) that produces no error at all when missing.
If you're shipping to both, write one service class with platform branches
rather than two implementations. In practice that means: one
InitializationSettings carrying both android: and iOS: entries, one
NotificationDetails carrying both, and a requestPermissions() that switches
on Platform.isAndroid / Platform.isIOS. Keep the notification config in a
single shared getter — duplicating it across your scheduling and test methods
is how the two platforms quietly drift apart.
| Android | iOS | |
|---|---|---|
| Pending limit | ~500 alarms | 64 — hard cap, silently drops the rest |
| Survives app being killed | Only after a battery-optimisation exemption | Always |
| Survives reboot | Needs a boot receiver | Automatic |
| Exact-timing permission | SCHEDULE_EXACT_ALARM / USE_EXACT_ALARM |
Not needed |
| Shows while app is open | Yes | No, by default |
| Extra native config | Manifest permissions + receivers, Gradle desugaring | AppDelegate.swift wiring |
| Custom sound | .mp3 in res/raw, new channel id to change it |
.aiff/.wav/.caf under 30 s, added to the Runner target |
| Biggest trap | Battery optimisation | The silent AppDelegate failure |
flutter_local_notifications^18.x- Flutter 3.44.7 stable
- Android SDK 36, Android 14–16 (tested on a Nothing Phone)
- Xcode 26.5, iOS 26 (tested on a physical iPhone)
Package APIs drift between major versions. If a signature has moved, check the changelog — the underlying concepts (exact alarms, desugaring, battery optimisation, the AppDelegate delegate, the 64 cap) stay the same.
- Steps4Perks — walking rewards app using the Android setup here
flutter_local_notificationson pub.dev — the package itself
Corrections and additions welcome — open an issue.