alarm_plus
alarm_plus is a Flutter alarm plugin focused on production reliability:
- Android: exact alarms + foreground ringing service + lock-screen/full-screen flow
- iOS: AlarmKit system alarms on iOS 26+ with notification-based best-effort fallback on iOS 15-25
It uses MethodChannel + EventChannel and does not depend on flutter_local_notifications internally.
Features
- Unified Dart API:
initializetriggerNowschedulecanceldeletestopsnoozegetAll
- Helper APIs:
eventsstream:triggered | stopped | snoozed | error | permissionChangedgetLaunchAlarm()getPermissionStatus()requestPermissions()
- Android Room persistence + reboot/package-replace reschedule.
- Android notification actions (
STOP,SNOOZE) work via receivers/service. - Deep UI & Sound Customization: Support for custom notification titles, bodies, custom action button texts, icons, big pictures, and custom audio assets (
.mp3/.wavfrom Flutter assets) for both platforms viaAlarmNotificationSettings. - URL-based Images: Load notification large icons and big pictures from HTTP/HTTPS URLs (Android uses Coil, iOS downloads synchronously).
- Vibration & Volume Customization:
- Android and iOS 15-25: set custom volume levels, fades, vibration presets, and continuous vibration.
- iOS 26+: AlarmKit owns the alert sound and haptics; the system alarm experience intentionally does not apply app-level volume or vibration settings.
Platform Behavior
| Capability | Android | iOS |
|---|---|---|
| Exact alarm timing | Yes (AlarmManager.setExactAndAllowWhileIdle) |
Yes on iOS 26+ (AlarmKit); best-effort local notification on iOS 15-25 |
| Foreground ringing service | Yes | No; AlarmKit owns iOS 26+ alert playback |
| Wake lock-managed playback | Yes | No |
| Full-screen/lock-screen path | Yes (full-screen intent + activity flags) | System AlarmKit UI on iOS 26+ |
| Reboot reschedule | Yes | N/A (notification requests survive per OS policy) |
Installation
Add dependency:
dependencies:
alarm_plus: ^0.1.0
Android setup
alarm_plus manifest includes required permissions and components. Host apps should still verify policy and UX:
POST_NOTIFICATIONS(Android 13+ runtime permission)SCHEDULE_EXACT_ALARM(Android 12+ exact alarm policy)USE_FULL_SCREEN_INTENT(Android 14+ capability gating)- Optional battery optimization exemptions for aggressive OEM firmware
For lock-screen experience, ensure your launcher/Flutter activity can show over lock screen. Example app sets:
setShowWhenLocked(true)setTurnScreenOn(true)
iOS setup
- iOS minimum target:
15.0. - On iOS 26+, request AlarmKit permission from app flow (
requestPermissions()). AlarmKit requires Xcode 26+ and an iOS 26+ device. - Add this required key to the host app's
Info.plist:<key>NSAlarmKitUsageDescription</key> <string>This app schedules alarms that you create.</string> - On iOS 15-25, request notification permission from app flow (
requestPermissions()). - Required for true background alarms: You must add the Audio Background Mode to your app's Xcode project. This allows alarms to ring out loud even if the physical silent switch is engaged and the app is in the background.
- Open
ios/Runner.xcworkspace. - Go to the
Runnertarget -> Signing & Capabilities. - Add Background Modes and check Audio, AirPlay, and Picture in Picture. (This automatically adds
UIBackgroundModes: audioto yourInfo.plist).
- Open
Permissions
Permission Status
Before scheduling alarms, check permission status:
final status = await AlarmPlus.getPermissionStatus();
print("Notifications: ${status.notificationsGranted}");
print("Exact Alarms (Android): ${status.exactAlarmsGranted}");
print("Full-Screen Intent (Android 14+): ${status.fullScreenIntentGranted}");
Platform Differences:
| Permission | Android | iOS |
|---|---|---|
notificationsGranted |
Post notification permission (Android 13+) | AlarmKit authorization on iOS 26+; notification authorization on iOS 15-25 |
exactAlarmsGranted |
SCHEDULE_EXACT_ALARM permission | Always false (not applicable) |
fullScreenIntentGranted |
USE_FULL_SCREEN_INTENT (Android 14+) | Always false (not applicable) |
Requesting Permissions
// Returns true if user granted permissions
final granted = await AlarmPlus.requestPermissions();
if (!granted) {
// Show dialog explaining why permissions are needed
// User can open app settings via platform channel if desired
}
On Android: After requestPermissions(), if exact alarms fail, user must enable "Schedule exact alarm" in app settings.
On iOS: requestPermissions() requests AlarmKit authorization on iOS 26+ and notification authorization on iOS 15-25.
Usage
import 'package:alarm_plus/alarm_plus.dart';
@pragma('vm:entry-point')
void receiveBackgroundNotification(NotificationResponse notificationResponse) {
// handle stop/snooze action in background isolate
}
Future<void> setup() async {
await AlarmPlus.initialize(
onDidReceiveNotificationResponse: (NotificationResponse response) {
// open your custom alarm screen
},
onDidReceiveBackgroundNotificationResponse: receiveBackgroundNotification,
);
await AlarmPlus.requestPermissions();
// Schedule alarm for local time (automatically converted to UTC internally)
final alarmTime = DateTime.now().add(const Duration(minutes: 2));
await AlarmPlus.schedule(
id: 'wake_up',
time: alarmTime, // Use local time — plugin handles UTC conversion
data: {'title': 'Morning alarm'},
notificationSettings: const AlarmNotificationSettings(
title: 'Wake Up!',
body: 'Time to start your day.',
stopButtonText: 'Dismiss',
snoozeButtonText: 'Snooze',
payload: 'custom_payload_data',
soundAsset: 'assets/audio/alarm.mp3', // Remember to add to pubspec.yaml
icon: 'ic_lock_idle_alarm', // Android drawable name
largeIconUrl: 'https://example.com/icon.png', // URL for large icon
bigPictureUrl: 'https://example.com/banner.jpg', // URL for big picture
volumeSettings: VolumeSettings(
volume: 0.8, // 80% volume
fadeDuration: Duration(seconds: 10), // Fade in over 10s
volumeEnforced: true, // Reset if user lowers volume
),
vibrationSettings: VibrationSettings(
enabled: true,
preset: VibrationPreset.strong,
continuous: true,
// For custom patterns:
// preset: VibrationPreset.custom,
// customPattern: [0, 1000, 500, 1000], // [wait, vibrate, wait, vibrate]
),
),
);
}
Volume Fading with Custom Steps
For advanced volume control, use VolumeFadeStep:
volumeSettings: VolumeSettings(
fadeSteps: [
VolumeFadeStep(volume: 0.1, at: Duration(seconds: 0)),
VolumeFadeStep(volume: 0.5, at: Duration(seconds: 30)),
VolumeFadeStep(volume: 1.0, at: Duration(minutes: 1)),
],
),
Time Handling: Always pass local DateTime to schedule(). The plugin automatically converts to UTC internally (scheduledTimeUtcMs). The scheduledTimeLocalIso field in AlarmModel is for UI display.
Trigger now
await AlarmPlus.triggerNow(
data: {'id': 'quick_alarm', 'source': 'debug'},
notificationSettings: const AlarmNotificationSettings(
title: 'Instant Alarm',
body: 'Triggered immediately.',
),
);
Listen for events
The events stream emits AlarmEvent objects with detailed metadata. Full event map structure:
final sub = AlarmPlus.events.listen((event) {
event.type; // "triggered" | "stopped" | "snoozed" | "error" | "permissionChanged"
event.id; // alarm ID (null for permissionChanged)
event.alarm; // full AlarmModel (null for permissionChanged)
event.errorCode; // for error type only
event.errorMessage; // for error type only
event.meta; // type-specific metadata (see below)
});
Event Types & Metadata:
triggered: Alarm fired.meta = {"driftMs": <ms delay from scheduled time>}— usedriftMsto monitor scheduling accuracy.stopped: User dismissed alarm.meta = {}snoozed: User snoozed alarm.meta = {"minutes": <snooze duration>}error: Scheduling failed (e.g., permission denied). ContainserrorCodeanderrorMessage.permissionChanged: User changed notification/alarm permissions.meta = entire PermissionStatus
Error Handling Example:
final sub = AlarmPlus.events.listen((event) {
if (event.type == "error") {
print("Alarm ${event.id} failed: ${event.errorCode} - ${event.errorMessage}");
// Log, retry, or show user dialog
}
});
Inspect and control alarms
final alarms = await AlarmPlus.getAll();
await AlarmPlus.snooze('wake_up', 5);
await AlarmPlus.stop();
await AlarmPlus.cancel('wake_up');
await AlarmPlus.delete('wake_up');
Background Notification Responses
When users tap stop/snooze buttons on an alarm notification while the app is in the background or killed, the callback function runs in a separate Dart isolate:
@pragma('vm:entry-point')
void receiveBackgroundNotification(NotificationResponse notificationResponse) {
// This runs in a SEPARATE isolate, not your main app isolate
// Main app state (shared variables, singletons) is NOT accessible here
// Only use this to log, update persistence, or call MethodChannel
print("Stop/Snooze tapped: ${notificationResponse.actionId}");
}
Key Constraints:
- Do NOT rely on static variables or singletons from main app
- Do NOT modify UI
- Do use MethodChannel if you need data from main isolate
- Do use top-level functions only — lambda closures won't be compiled as background entry points
- Always include
@pragma('vm:entry-point')above the function declaration
AlarmModel
Represents the full state of a scheduled or triggered alarm.
Fields:
id(String): Unique identifier for the alarmscheduledTimeUtcMs(int): Scheduled trigger time as milliseconds since epoch (UTC). Always in UTC; convert to local time for display.scheduledTimeLocalIso(String): ISO8601 local time string for UI display (e.g., "2026-05-18T07:30:00.000")payloadJson(String?): App-specific JSON data passed to callbackstatus(String): Current state:scheduled|triggered|snoozed|stopped|canceled|errorcreatedAtMs,updatedAtMs(int): Record creation/update timestampslastTriggeredAtMs(int?): When the alarm actually fired (may differ from scheduled time)lastDriftMs(int?): Drift tracking — difference in milliseconds:(lastTriggeredAtMs - scheduledTimeUtcMs). Use to monitor scheduler accuracy. Positive = late, negative = early.retryCount(int): Number of retry attempts (Android only; bounded retry metadata)nextRetryAtMs(int?): Timestamp of next retry attempt (if applicable)platformMeta(Map): Platform-specific runtime metadata (e.g., Android notification ID, iOS notification request ID)
Usage Example (check drift):
final alarm = await AlarmPlus.getById('wake_up');
if (alarm != null && alarm.lastDriftMs != null) {
print("Alarm triggered ${alarm.lastDriftMs}ms ${alarm.lastDriftMs! > 0 ? 'late' : 'early'}");
}
Reliability Notes
Android path is reliability-first:
- exact alarm scheduling
- receiver -> immediate foreground service handoff
- wake-lock-backed playback
- persisted alarm state transitions
- boot/package/time-change reschedule
- trigger drift metrics persisted in model
- bounded retry metadata (
retryCount,nextRetryAtMs)
iOS uses a versioned approach:
- iOS 26+: Native AlarmKit schedules the system-managed alarm UI, sound, and lifecycle. AlarmKit works through Focus and Silent mode without a Flutter dependency.
- iOS 15-25: Local notifications and the existing best-effort AVAudioPlayer path remain the fallback.
- The Dart
snooze()API reschedules the current alarm. iOS 26+ uses the standard AlarmKit Stop action; no countdown widget extension is required.
Troubleshooting
- If Android alarms are delayed on vendor ROMs: verify battery/background restrictions for the app.
- If exact alarms fail: check
getPermissionStatus().exactAlarmsGranted. - If full-screen doesn’t appear on Android 14+: check
fullScreenIntentGrantedand open settings viarequestPermissions(). - If iOS 26+ alarms fail: verify
NSAlarmKitUsageDescription, AlarmKit authorization, Xcode 26+, and a physical iOS 26+ device. - If iOS 15-25 alarms do not alert: verify notification authorization and system notification settings.
Example
example/ includes a runnable app for:
- schedule
- trigger now
- snooze
- stop
- cancel
- permissions/status
- event stream logs
- launch-alarm handoff
References and Learnings
1) alarm
Source: pub.flutter-io.cn/packages/alarm
2) flutter_local_notifications
Source: pub.flutter-io.cn/packages/flutter_local_notifications
-
Appreciate my work? Show some ❤️ and star the repo to support this package.
-
For more information about the properties, look at the API reference.