NotificationService — platform abstraction over scheduling, cancelling, showing, and permissions (OS notifications; see the glossary for the reminder/notification split).
FlutterLocalNotificationsService — concrete implementation using flutter_local_notifications and timezone.
notificationAlarmCallback (data/services/android_alarm_callback.dart) — top-level background entry point used by android_alarm_manager_plus on Android; shows the notification and reschedules the next fire from a background Dart isolate.
ReminderScheduler — pure, timezone-aware nextFireTime calculation for fixed, interval, and semi-random schedules.
Reminder and ReminderSchedule — typed domain models with JSON round-trip.
RemindersRepository and ListsRepository — Drift-backed CRUD and watch streams.
Riverpod providers expose the initialized service, live reminder lists, and lists.
platform limits
iOS/macOS — each reminder is a single one-shot zonedSchedule (_useZonedSchedule). there is no background isolate on these platforms, so the next occurrence is computed by ReminderSchedulerWatcher's reschedule pass, which runs on app launch/resume and whenever the reminders table changes. a reminder can therefore go quiet while the app stays closed — there is no pre-computed batch of future fire times.
Android 12+ — exact-alarm permission and battery-optimization exemption are only requested when a reminder's alarmMode is enabled. alarmMode on uses AlarmManager.setAlarmClock for precise, high-priority delivery and appears in the system alarm clock; alarmMode off (the default) uses AlarmManager.set for best-effort inexact delivery that does not wake the device from Doze. in both cases a background Dart callback (notificationAlarmCallback) shows the notification and reschedules the next one, so reminders keep firing when the app is closed.
Linux/Windows — the desktop notification plugin cannot reliably wake a background process, so an in-app Timer shows the notification and reschedules the next one while the app is running. reminders do not survive a full quit on these platforms (except on Linux/Windows builds where the app minimizes to the tray and keeps running).
runtime behavior
FlutterLocalNotificationsService detects the system IANA timezone from /etc/timezone, /etc/localtime, or timedatectl.
on Android, android_alarm_manager_plus.oneShotAt is used; the top-level callback shows the notification and reschedules the next one from a background Dart isolate.
reminders with alarmMode enabled use setAlarmClock for exact, high-priority delivery and appear in the system alarm clock.
reminders with alarmMode disabled (the default) use set for best-effort inexact delivery without populating the system alarm clock or waking the device from Doze.
on iOS/macOS, flutter_local_notifications.zonedSchedule is used for a single next occurrence; the following occurrence is scheduled by the next reschedule pass.
on Linux and Windows, an in-app Timer calls show() and reschedules the next one while the process is alive.
MinimizeToTrayInitializer (core/window) wires nativeapi window and tray listeners, shows the last notification on left click, and provides a show/hide context menu on right click.
preventing duplicate or stale reminders
ReminderSchedulerWatcher serializes reschedule passes in a single queue. if the reminder list changes while a pass is in flight, the change is coalesced into one follow-up pass rather than interleaving with the in-flight one.
the Android background callback and the desktop Timer callback both load the current reminder from the database before showing or rescheduling. if the reminder was deleted or disabled since the alarm/timer was set, the callback cancels the alarm and stops.
ReminderScheduler parses fixed-time schedule strings into TZDateTime values and sorts them chronologically, so the next fire time is never chosen based on lexicographic string order.
semi-random schedules seed their random walk from the reminder id and the window's calendar day, so the day's candidate lattice is fixed: the fire callback and ReminderSchedulerWatcher reschedules always agree on the same next point, and consecutive fires stay intervalMinutes ± varianceMinutes apart. a fresh Random() per call would re-roll the lattice on every reschedule and collapse the spacing to a residual of the new walk.
the reminder editor saves the reminder first, then runs permission checks and the saved snackbar. if any of those post-save steps fail, the error is logged but the AutoSaveDraft still receives the saved model so a retry updates the same row instead of inserting a second one.