warm_alarm 0.1.9
warm_alarm: ^0.1.9 copied to clipboard
An independent Flutter alarm plugin extracted from WarmWake's core alarm functionality.
warm_alarm #
A platform-honest alarm plugin for Flutter — schedules reliable alarms on Android, iOS, and macOS with a capability/permission/readiness model that tells you exactly what the current platform can deliver before you commit to a schedule.
Background #
warm_alarm was extracted from the core alarm functionality of WarmWake.
It is maintained as an independent Flutter plugin and does not require the WarmWake app.
Features #
- Capability-first design — feature support is reported at runtime, with platform-specific limits documented below
- Three-level system inspection —
getCapabilities(),getPermissionState(), andgetReadiness()before you schedule - Reactive event stream — sealed
WarmAlarmEventtypes covering every alarm lifecycle transition - Custom audio — local file, asset, or platform default with fade steps, looping, and platform-specific vibration
- Wake-check flow (Android only) — verifies the user is awake after dismissal and retriggers if not
- Snooze and recurrence — built-in snooze duration and weekly recurrence by weekday
- Federated plugin — independent implementations for Android, iOS, and macOS
Installation #
Add warm_alarm to your Flutter app.
This checkout prepares facade 0.1.9; that release must be published before the constraint below resolves from pub.dev.
dependencies:
warm_alarm: ^0.1.9
Then run:
flutter pub get
Quick Start #
import 'package:warm_alarm/warm_alarm.dart';
// 1. Check whether the platform can deliver a reliable alarm.
final readiness = await WarmAlarm.getReadiness();
if (readiness.level == WarmAlarmReadinessLevel.blocked) {
// Surface readiness.reasons to the user so they can fix permissions.
return;
}
// 2. Schedule an alarm with audio, snooze, and a wake-check follow-up.
final result = await WarmAlarm.scheduleAlarm(
WarmAlarmSchedule(
id: 1,
scheduledAt: DateTime.now().add(const Duration(minutes: 30)),
notification: const WarmAlarmNotification(
title: 'Wake up!',
body: 'Good morning',
stopActionTitle: 'Stop',
snoozeActionTitle: 'Snooze',
),
audio: const WarmAlarmAudio(loop: true, vibrate: true),
snooze: const WarmAlarmSnooze(duration: Duration(minutes: 5)),
wakeCheck: const WarmAlarmWakeCheck(
checkDelay: Duration(minutes: 2),
retriggerDelay: Duration(minutes: 1),
),
),
);
print('Scheduled alarm ${result.alarmId}, readiness: ${result.readiness.level}');
for (final warning in result.warnings) {
print('Warning (${warning.code?.name ?? 'unclassified'}): ${warning.message}');
}
// 3. React to alarm lifecycle events.
WarmAlarm.events.listen((event) {
switch (event) {
case WarmAlarmFired(): print('Alarm ${event.alarmId} fired');
case WarmAlarmStopped(): print('Alarm ${event.alarmId} stopped');
case WarmAlarmSnoozed(): print('Snoozed for ${event.duration}');
default: break;
}
});
// 4. Cancel an alarm.
await WarmAlarm.cancelAlarm(1);
Platform Capabilities #
| Feature | Android | iOS | macOS |
|---|---|---|---|
| Notification scheduling | ✅ Full | ✅ Full | ✅ Full |
| Exact alarm scheduling | ✅ Full | ✅ or ⚠️ | ❌ Unsupported |
| Background audio playback | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited |
| Full-screen presentation | ✅ Full | ❌ None | ❌ None |
| Wake-check | ✅ Full | ❌ None | ❌ None |
| Custom Live Activities | ❌ None | Opt-in host integration | ❌ None |
⚠️ Limited means the native implementation reports conditional support.
Capabilities are runtime snapshots: iOS exact scheduling also depends on host configuration and AlarmKit authorization.
Call getReadiness() before scheduling; neither a supported capability nor a ready snapshot guarantees future alarm delivery.
Surface the reasons list so users can address detected permissions or platform limits.
Android reports limited when notification, exact-alarm, or full-screen permission is missing, and ready when all three are granted.
Apple's User Notifications backend reports blocked without notification permission and limited with it because background execution remains constrained.
An authorized iOS AlarmKit backend can report ready without notification permission; active User Notifications fallback schedules still require notification permission.
Not every reason has a supported settings action.
iOS User Notifications setup #
Every consuming iOS target must enable the Time Sensitive Notifications capability and include the following entitlement before it relies on Focus delivery for User Notifications alarms:
<key>com.apple.developer.usernotifications.time-sensitive</key>
<true/>
On iOS, inspect WarmAlarmReadiness.notificationSettings before selecting a remediation reason.
Full authorization with alerts, sounds, and Time Sensitive Notifications enabled needs no notification-settings guidance.
Provisional authorization or a disabled alert, sound, or Time Sensitive setting needs notification-settings guidance through openReadinessSettings(WarmAlarmReadinessReason.backgroundExecutionLimited).
Prioritize that reason over an earlier exact-alarm reason when both issues exist.
When these notification settings are healthy, skip backgroundExecutionLimited and select the first other supported reason, if any.
A null notification-settings snapshot means that the current platform implementation does not report granular settings.
iOS 26 AlarmKit setup #
Add a non-empty NSAlarmKitUsageDescription to the host app's Info.plist to opt in to AlarmKit on iOS 26 or later.
The first native schedule can show the AlarmKit authorization prompt.
The plugin falls back to User Notifications when AlarmKit is unavailable, unconfigured, denied, or fails to schedule and native cleanup succeeds.
It returns an error instead of installing a second backend when native cleanup cannot be confirmed.
getCapabilities() reports exact scheduling as supported only when the runtime and host configuration allow the AlarmKit backend.
The system AlarmKit UI does not open an arbitrary Flutter full-screen route.
AlarmKit Snooze uses a post-alert countdown.
Apple requires a Widget Extension with an ActivityConfiguration for AlarmAttributes<Never> when the host enables this countdown.
Declare extension Never: @retroactive AlarmMetadata {} in the extension so it uses the same module-independent empty metadata type as the plugin.
Set both NSSupportsLiveActivities and WarmAlarmAlarmKitLiveActivityEnabled to true in the app target's Info.plist only after the extension is ready.
Until then, schedules with Snooze use the User Notifications fallback and return a warning.
Call prepareSystemSound with a recording and optional tone, then pass its returned path as WarmAlarmAudio.systemSoundFilePath.
Keep the legacy audio inputs for fallback, and start app audio only when the saved snapshot does not report systemManagedAudio.
Unsupported hosts return null; preparation failures throw without replacing an existing alarm.
See the warm_alarm_ios host requirements for the complete value mapping, preparation limits, file lifetime, and event limitations.
Event delivery #
Native lifecycle events enter a durable FIFO before the plugin sends them to Dart.
Android commits the queue to device-protected preferences, and Apple platforms atomically replace an app-scoped Application Support file before emission.
Apple queue files remain protected until the first device unlock.
WarmAlarm.init() registers the Dart callback before requesting native replay.
Each platform queue retains up to 64 events and removes an event only after the callback succeeds.
Callback failures stop the drain and retain the event for a later initialization or new event.
The oldest event is dropped when the queue reaches capacity.
Unreadable root data is replaced with an empty queue, while malformed records in a readable queue are discarded individually.
Android imports pending Snooze events from the previous store.
Events received while no stream listener is attached are buffered in Dart, also up to 64 entries. The native callback remains pending until a listener attaches, unless the oldest event is evicted by the buffer limit. Subscribe before initialization when possible, and keep the subscription active for the session. Delivery is at least once across a crash between a successful callback and persistent acknowledgement, so consumers should tolerate repeated events. Atomic file replacement protects against process termination after a completed write; it does not promise recovery from hardware failure or power loss before filesystem buffers are flushed. The queue cannot reconstruct events that the operating system never delivered while the process was absent.
Custom Live Activities #
Configured iOS hosts can call startLiveActivity, updateLiveActivity, and endLiveActivity.
These operations display alarm status without changing the alarm schedule or starting audio.
The host must register an ActivityKit adapter and supply a Widget Extension that shares the activity attributes with the app.
See the iOS setup and complete Swift sample.
The API returns unsupported on Android, macOS, older iOS versions, and unconfigured hosts, or disabled when the user has disabled Live Activities.
Custom activities use a separate opt-in from AlarmKit countdown presentation.
API Reference #
WarmAlarm — static entry point #
| Method | Returns | Description |
|---|---|---|
init() |
Future<void> |
Rehydrate native alarm state after a process restart |
startLiveActivity(state) |
Future<WarmAlarmLiveActivityResult> |
Start a custom activity on a configured iOS host |
updateLiveActivity(id, state) |
Future<WarmAlarmLiveActivityResult> |
Update activity content without rescheduling the alarm |
endLiveActivity(id) |
Future<WarmAlarmLiveActivityResult> |
End a custom activity by its identifier |
prepareSystemSound(...) |
Future<String?> |
Prepare an AlarmKit sound; return null if unsupported |
getCapabilities() |
Future<WarmAlarmCapabilities> |
Per-feature support status for the current platform |
getPermissionState() |
Future<WarmAlarmPermissionState> |
Current notification and exact-alarm permission grants |
getReadiness() |
Future<WarmAlarmReadiness> |
Overall system readiness with actionable reason codes |
requestNotificationPermission() |
Future<WarmAlarmRemediationResult> |
Request notification authorization and return current state |
openReadinessSettings(reason) |
Future<WarmAlarmRemediationResult> |
Open supported native settings and return current state |
scheduleAlarm(schedule) |
Future<WarmAlarmScheduleResult> |
Schedule an alarm; returns the assigned ID and any warnings |
cancelAlarm(id) |
Future<void> |
Cancel a specific alarm by ID |
cancelAllAlarms() |
Future<void> |
Cancel all scheduled alarms |
getScheduledAlarms() |
Future<List<WarmAlarmSnapshot>> |
List all currently scheduled alarms |
hasAlarm() |
Future<bool> |
Whether any future alarm is currently scheduled |
getAlarm(id) |
Future<WarmAlarmSnapshot?> |
Fetch a single future scheduled alarm by ID |
isRinging({id}) |
Future<bool> |
Whether an alarm (or any alarm) is currently ringing |
setKillWarning(...) |
Future<void> |
Configure app-termination warning text |
clearKillWarning() |
Future<void> |
Clear the configured warning text |
events |
Stream<WarmAlarmEvent> |
Real-time alarm lifecycle event stream |
setKillWarning() configures warning text for later lifecycle callbacks; it does not immediately display a notification.
clearKillWarning() removes that configuration without removing a warning that is already pending or delivered.
See the platform-specific triggers and removal behavior for Android, iOS, and macOS.
On iOS and macOS, a schedule with wakeCheck returns warning.code == WarmAlarmWarningCode.unsupportedWakeCheck because wake-check cannot run there.
The wake-check request is ignored.
The published iOS 0.1.13 and macOS 0.1.8 releases keep native scheduling warnings as separate entries in result.warnings, after the wake-check entry.
Facade 0.1.9 requires interface 0.1.8 and those implementations, together with Android 0.1.6.
The legacy warning getter returns the first entry; earlier Apple versions retain native messages inside that single warning instead.
unsupportedWakeCheck is currently the only structured warning code; other native warnings can have a null code.
Warnings do not cover every ignored option; see the audio contract for reserved fields and platform-specific behavior.
openReadinessSettings(reason) hands the user off to a system screen and returns as soon as the
platform accepts the request, so its snapshot describes the state before the remediation — call
getReadiness() again once the app resumes to see the outcome.
requestNotificationPermission() awaits the system dialog, so its snapshot already reflects the
user's answer.
Key data classes #
| Class | Purpose |
|---|---|
WarmAlarmSchedule |
id, scheduledAt, notification, audio, recurrence?, snooze?, wakeCheck?, payload?, androidFullScreenIntent (default true) |
WarmAlarmCapabilities |
WarmAlarmSupportStatus for notification scheduling, exact scheduling, background audio, full-screen intents, wake-check, and Live Activity |
WarmAlarmReadiness |
level, reasons, and optional notificationSettings |
WarmAlarmNotificationSettings |
authorizationStatus, alertsEnabled, soundsEnabled, and nullable timeSensitiveEnabled |
WarmAlarmPermissionState |
notificationsGranted, exactAlarmGranted, fullScreenIntentGranted |
WarmAlarmRemediationResult |
status, permissionState, readiness |
WarmAlarmScheduleResult |
alarmId, readiness, read-only warnings, and warning (first entry or null) |
WarmAlarmWarning |
message, optional code; a null code still represents a warning |
WarmAlarmFailure |
code, optional message |
WarmAlarmAudio |
filePath?, assetPath?, systemSoundFilePath?, loop (default true), volume?, fadeInDuration?, fadeSteps?, volumeEnforced (default false), vibrate (default true) |
WarmAlarmNotification |
title, body, stopActionTitle?, snoozeActionTitle?, androidIcon?, androidIconColor?, keepNotificationAfterAlarmEnds (default false) |
WarmAlarmSnooze |
duration |
WarmAlarmRecurrence |
weekdays: ISO weekday numbers from 1 (Monday) through 7 (Sunday) |
WarmAlarmWakeCheck |
checkDelay, retriggerDelay?, maxRetriggers? (default 1, Android only) |
WarmAlarmVolumeFadeStep |
time (offset from alarm start), volume (0.0–1.0) |
WarmAlarmSnapshot |
Schedule fields plus systemManagedAudio (default false), which reports actual system playback ownership |
WarmAlarmLiveActivityState |
alarmId, title, status, optional scheduledAt; content for a host-provided widget, not an alarm schedule |
WarmAlarmLiveActivityResult |
status, optional activityId |
Scheduling validation #
WarmAlarm.scheduleAlarm validates the request before it reaches a platform implementation.
| ArgumentError name | Rejected value |
|---|---|
schedule.scheduledAt |
A one-time time that is not strictly future at millisecond precision |
schedule.recurrence.weekdays |
An empty list or a weekday outside ISO 1–7 |
schedule.snooze.duration |
A negative duration |
schedule.wakeCheck.checkDelay |
A negative duration |
schedule.wakeCheck.retriggerDelay |
A negative duration |
schedule.wakeCheck.maxRetriggers |
A negative count |
schedule.audio.volume |
A non-finite value or a value outside 0.0–1.0 |
schedule.audio.fadeInDuration |
A negative duration |
schedule.audio.fadeSteps.time |
A negative value or a value that is not strictly later than the preceding step at millisecond precision |
schedule.audio.fadeSteps.volume |
A non-finite value or a value outside 0.0–1.0 |
A recurring schedule may use a past scheduledAt anchor because its next local weekday and time determine its next occurrence.
Audio contract #
filePath and assetPath are valid custom sources.
An empty string means that source is absent.
When both are supplied, an accessible filePath takes precedence.
When neither is supplied, the platform uses its native notification or alarm sound.
The backgroundAudioPlayback capability is limited, so check getCapabilities() and getReadiness() before relying on custom audio after the app backgrounds.
| Field | Contract and platform behavior |
|---|---|
filePath |
Local custom audio file. Android selects it before an asset when it can read the file; iOS and macOS use it for their plugin-owned player. |
assetPath |
Flutter asset used when filePath is absent or inaccessible. Android can use it after a reboot before credential-protected files are available. |
systemSoundFilePath |
iOS AlarmKit override returned by prepareSystemSound. Keep filePath or assetPath for fallback. Android and macOS ignore it. |
loop |
Requests repeated custom playback. Android applies it to file and asset sources; its native fallback alarm loops. |
volume, fadeSteps |
Custom-player gain controls. Volume values are 0.0–1.0. Fade timestamps are nonnegative and strictly increasing at millisecond precision. |
fadeInDuration |
Reserved hint retained in native schedule data. Current Android, iOS, and macOS playback paths do not apply it; use fadeSteps for a fade curve. |
volumeEnforced |
Android attempts to enforce the alarm stream volume. iOS and macOS can enforce only their plugin-owned player volume, so Apple system volume is not guaranteed. |
vibrate |
Android requests alarm vibration. Apple background haptics are unsupported for scheduled alarms. |
Without a custom source, Android uses the system alarm sound and falls back to the notification sound when necessary. iOS and macOS use their native notification or AlarmKit sound behavior.
WarmAlarmEvent — sealed event types #
| Subtype | Payload |
|---|---|
WarmAlarmScheduled |
alarmId, occurredAt |
WarmAlarmFired |
alarmId, occurredAt, payload? |
WarmAlarmStopped |
alarmId, occurredAt, payload? |
WarmAlarmSnoozed |
alarmId, occurredAt, duration, payload? |
WarmAlarmFailed |
alarmId, occurredAt, failure (WarmAlarmFailure) |
WarmAlarmWakeCheckShown |
alarmId, occurredAt (Android) |
WarmAlarmWakeCheckDismissed |
alarmId, occurredAt (Android) |
WarmAlarmWakeCheckExpired |
alarmId, occurredAt (Android) |
WarmAlarmRetriggered |
alarmId, occurredAt, payload? (Android) |
Migrating from alarm #
warm_alarm is a drop-in design replacement for the alarm package,
with a richer capability model, macOS support, and a cleaner public/wire boundary.
Why switch? #
| Concern | alarm |
warm_alarm |
|---|---|---|
| Platform coverage | Android + iOS | Android + iOS + macOS |
| Pre-schedule diagnosis | None — silent failures | getCapabilities() + getPermissionState() + getReadiness() with actionable reason codes |
| Schedule result | bool success flag |
WarmAlarmScheduleResult — includes readiness, a warning list, and the legacy first warning |
| Wake verification | None | WarmAlarmWakeCheck — detects if the user actually woke up; retriggers the alarm if not |
| Snooze | App-layer only | Built-in WarmAlarmSnooze; notification action handled natively |
| Event model | ringStream / updateStream (deprecated) |
Stream<WarmAlarmEvent> — 9 typed events in a sealed class hierarchy |
| Store location | Dart-side SharedPreferences |
Native (SharedPreferences / UserDefaults) — survives Flutter engine restarts |
| Snapshot richness | Full AlarmSettings from Dart store |
Full WarmAlarmSnapshot reconstructed from native store via getScheduledAlarms() |
| Pigeon boundary | Single shared schema for all platforms | Per-platform schema — iOS and Android can diverge safely |
API mapping #
alarm |
warm_alarm |
Notes |
|---|---|---|
Alarm.set(alarmSettings:) |
WarmAlarm.scheduleAlarm(schedule) |
Returns WarmAlarmScheduleResult instead of bool |
Alarm.stop(id) |
WarmAlarm.cancelAlarm(id) |
|
Alarm.stopAll() |
WarmAlarm.cancelAllAlarms() |
|
Alarm.getAlarms() |
WarmAlarm.getScheduledAlarms() |
Returns full WarmAlarmSnapshot list |
Alarm.isRinging([id]) |
WarmAlarm.isRinging(id: id) |
|
Alarm.setWarningNotificationOnKill |
WarmAlarm.setKillWarning(title:body:) |
Paired with clearKillWarning() |
Alarm.scheduled / Alarm.ringing |
WarmAlarm.events |
Build a StateNotifier/Bloc on top of the event stream |
AlarmSettings.volumeSettings |
WarmAlarmAudio (volume, fadeInDuration, fadeSteps, volumeEnforced) |
|
NotificationSettings.icon |
WarmAlarmNotification.androidIcon |
Android only |
NotificationSettings.iconColor |
WarmAlarmNotification.androidIconColor |
Android only — ARGB int instead of Color |
AlarmSettings.payload |
WarmAlarmSchedule.payload |
Optional payload on the four event types listed below |
WarmAlarmSchedule.payload is exposed as an optional payload only on WarmAlarmFired, WarmAlarmStopped, WarmAlarmSnoozed, and WarmAlarmRetriggered events.
License #
BSD-3-Clause — Copyright (c) 2026, Dongmin Yu. See LICENSE for details.