Skip to main content

Mobile Development9 min read

Flutter Local Notifications: Full Guide

Schedule reliable daily reminders in Flutter with flutter_local_notifications: permissions, exact alarms, timezone-aware repeats, and tap handling.

  • #flutter
  • #local-notifications
  • #notifications
  • #android
  • #ios
Flutter Local Notifications: Full Guide

A reminder that fires at the right time is worth more than a beautiful screen nobody opens. Prayer times, medication doses, habit streaks, and wake-up alarms all depend on one unglamorous piece of infrastructure: a notification scheduled on the device itself, with no server involved. Flutter has a mature package for exactly this, and once you understand its permission model, it is straightforward to get right.

This guide covers the full setup: initializing the plugin, asking for permission the way Android 13 and iOS expect, showing an instant notification, scheduling a daily reminder that repeats, and handling the tap when the user opens the app from the notification. It closes with the lessons from shipping reminders in real apps, including the prayer-time notification system in Bayaan and the smart alarms in Rise Baby.

Local notifications versus push notifications

Local notifications are created and delivered entirely on the device. Your Dart code decides the time, the device fires it, and no network call is needed. Push notifications arrive from a server through Firebase Cloud Messaging or Apple Push Notification service, which means backend infrastructure, device tokens, and a delivery path you do not fully control.

Choose local notifications when the trigger is knowable on the device: a daily reminder at 7:00 AM, a prayer time computed from the calendar, a weekly habit check-in. Choose push when the trigger lives on a server: a message from another user, a price alert, a school announcement. Many apps need both. If you are weighing the server side, the companion guide Flutter Push Notifications with Firebase: Full Guide covers the FCM setup, and the exact-alarm details behind reliable scheduling are in Flutter Exact Alarm Scheduling on Android.

Setup

Add the plugin and the timezone package. The timezone package matters because a daily reminder must fire at 7:00 AM in the user's timezone, not 7:00 AM UTC, and it must survive daylight saving changes.

flutter pub add flutter_local_notifications timezone flutter_timezone

On Android, declare the permissions in android/app/src/main/AndroidManifest.xml. The notification permission is required on Android 13 and later, the exact-alarm permission is required if you schedule exact alarms, and the boot permission lets you re-create schedules after a restart, since alarms do not survive a reboot on their own.

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

On iOS there is no manifest work. Permission is requested at runtime, and the plugin handles the system prompt. One iOS limit worth knowing up front: the system keeps at most 64 scheduled notifications per app, so schedule a rolling window of upcoming reminders rather than a whole year at once.

Initialize the plugin

Initialize once, before the app runs, and configure the timezone database from the device timezone. The initialization settings pick an icon on Android and request no permissions yet on iOS — permission gets its own explicit step below, so the user sees the prompt in context rather than at first launch.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

final FlutterLocalNotificationsPlugin notifications =
    FlutterLocalNotificationsPlugin();

Future<void> initNotifications({
  required void Function(NotificationResponse response) onTap,
}) async {
  tz.initializeTimeZones();
  final String timeZoneName = await FlutterTimezone.getLocalTimezone();
  tz.setLocalLocation(tz.getLocation(timeZoneName));

  const androidSettings =
      AndroidInitializationSettings('@mipmap/ic_launcher');
  const iosSettings = DarwinInitializationSettings();
  const settings = InitializationSettings(
    android: androidSettings,
    iOS: iosSettings,
  );

  await notifications.initialize(
    settings,
    onDidReceiveNotificationResponse: onTap,
  );
}

Call this from main() before runApp(), and re-create your daily schedules there too. Re-creating a schedule with the same id replaces the old one, so startup re-scheduling is idempotent and it repairs anything a reboot cleared.

Ask for permission at the right moment

Never ask on first launch before the user understands why. Ask when they enable reminders, and explain what the reminder does first. On Android 13 and later the system permission is a runtime prompt. Exact alarms are a separate permission on Android 12 and later, and the user grants it from a system settings page, not a dialog, so check whether it is already granted and explain where to enable it if it is not.

Future<void> requestNotificationPermissions() async {
  final android = notifications.resolvePlatformSpecificImplementation<
      AndroidFlutterLocalNotificationsPlugin>();
  await android?.requestNotificationsPermission();

  final bool canScheduleExact =
      await android?.canScheduleExactNotifications() ?? false;
  if (!canScheduleExact) {
    await android?.requestExactAlarmsPermission();
  }

  final ios = notifications.resolvePlatformSpecificImplementation<
      IOSFlutterLocalNotificationsPlugin>();
  await ios?.requestPermissions(alert: true, badge: true, sound: true);
}

If exact-alarm permission is denied, do not fail silently. Fall back to an inexact schedule mode, which needs no special permission but may fire later than requested, and tell the user the reminder may be delayed. A late reminder the user understands beats a missing one they trusted.

Show an instant notification

An instant notification is the fastest way to verify the whole chain: initialization, channel, permission, and tap handling. Use a stable channel id and a human-readable channel name, because on Android the user manages sound, vibration, and visibility per channel, and the channel is created on first use.

Future<void> showInstantNotification() async {
  const details = NotificationDetails(
    android: AndroidNotificationDetails(
      'reminders',
      'Reminders',
      channelDescription: 'Daily reminders and alarms',
      importance: Importance.high,
      priority: Priority.high,
    ),
    iOS: DarwinNotificationDetails(),
  );

  await notifications.show(
    0,
    'Reminder set',
    'Your daily reminder is scheduled for 7:00 AM.',
    details,
  );
}

If nothing appears, check the permission first, then the channel. On Android, a channel created with low importance stays quiet even if you later raise the importance in code — the channel settings from first creation win, and only the user can change them in system settings. During development, uninstall the app or use a fresh channel id after changing importance.

Schedule a daily reminder

Daily reminders use zonedSchedule with DateTimeComponents.time, which repeats the notification at the same clock time every day. Compute the next occurrence in the device timezone, and let the match component handle the repetition rather than scheduling 365 separate notifications and hitting the iOS limit.

tz.TZDateTime nextDailyAt(int hour, int minute) {
  final now = tz.TZDateTime.now(tz.local);
  var scheduled = tz.TZDateTime(
    tz.local,
    now.year,
    now.month,
    now.day,
    hour,
    minute,
  );
  if (scheduled.isBefore(now)) {
    scheduled = scheduled.add(const Duration(days: 1));
  }
  return scheduled;
}

Future<void> scheduleDailyReminder({
  required int id,
  required String title,
  required String body,
  required int hour,
  required int minute,
  String? payload,
}) async {
  const details = NotificationDetails(
    android: AndroidNotificationDetails(
      'reminders',
      'Reminders',
      channelDescription: 'Daily reminders and alarms',
      importance: Importance.high,
      priority: Priority.high,
    ),
    iOS: DarwinNotificationDetails(),
  );

  await notifications.zonedSchedule(
    id,
    title,
    body,
    nextDailyAt(hour, minute),
    details,
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
    matchDateTimeComponents: DateTimeComponents.time,
    payload: payload,
  );
}

A few details in this snippet do real work. exactAllowWhileIdle lets the alarm fire during Doze mode, which is what makes a 7:00 AM reminder trustworthy on Android. The timezone-aware date keeps the reminder at 7:00 AM local time across daylight saving changes. The stable id per reminder means rescheduling replaces rather than duplicates, and cancel(id) removes exactly that reminder when the user turns it off.

To cancel one reminder or all of them:

await notifications.cancel(7); // one reminder, by id
await notifications.cancelAll(); // every scheduled and shown notification

Handle the tap, including cold start

A tap handler has three cases, and most bugs come from handling only the first. The app is in the foreground, the app is in the background, or the app is fully closed. The callback passed to initialize covers the first two. The closed case needs a separate check at startup, because the tap launched the app and the callback alone does not replay it.

void onNotificationTap(NotificationResponse response) {
  final String? payload = response.payload;
  if (payload == null) return;
  // Route from the payload, for example 'reminder:morning' or a screen id.
  // Keep the payload a small string, not a serialized object.
}

Future<void> handleColdStartTap() async {
  final details =
      await notifications.getNotificationAppLaunchDetails();
  if (details != null && details.didNotificationLaunchApp) {
    final response = details.notificationResponse;
    if (response != null) onNotificationTap(response);
  }
}

Keep payloads small and stable: a screen identifier or a reminder id. The payload is not a place for user data, and it must still make sense if the app state has changed since the notification was scheduled.

Lessons from shipping reminders in real apps

Bayaan is an Islamic lifestyle app with Adhan casting, prayer times, daily reminders, and prophetic routines, and its case study records a comprehensive prayer times and notification system. Prayer times shift every day, which forces the pattern above: compute the next occurrence, schedule it, and re-create schedules at startup. A fixed daily time would have been simpler and wrong.

Rise Baby takes the same infrastructure in a stricter direction. It is a smart alarm app with snooze control and wake-up challenges, built on a reliable background alarm scheduling system with local notifications in its stack. An alarm the user sleeps through is a failed feature, so exact scheduling, Doze-mode behavior, and the fallback when exact permission is denied all matter more here than in a casual reminder. The lesson generalizes: the more the user depends on the notification, the more you should test the denied-permission path, the reboot path, and the battery-optimization path, not just the happy path.

One more habit from both apps: give every reminder a settings screen where the user can change the time, the sound, and whether it fires at all. A notification the user cannot control becomes a notification the user disables entirely, and a disabled channel is much harder to win back than a retimed reminder.

Troubleshooting

SymptomMost likely causeFix
Nothing appears on Android 13 or laterNotification permission not grantedRequest it in context and check the app settings
Fires late on AndroidExact-alarm permission denied, inexact mode in useRequest exact-alarm permission or accept the delay and say so
Notification is silentChannel created with low importanceUse a new channel id or have the user change it in settings
Daily reminder fires once, then stopsScheduled without a match componentPass DateTimeComponents.time for a daily repeat
Fires at the wrong hour after travelTimezone never configuredInitialize the timezone database and set the device location
Tap does nothing after a cold startOnly the stream callback is handledCheck getNotificationAppLaunchDetails at startup
iOS reminder never firesPermission denied or past the 64-notification limitRequest permission again and schedule a rolling window

Pre-launch checklist

Before you ship, confirm the chain end to end on a real device, not only an emulator: permission prompt in context, instant notification visible, daily reminder scheduled with the correct next fire time, tap routing from foreground, background, and cold start, behavior after a reboot, and the exact-permission-denied fallback. Local notifications feel simple because there is no server, but their failure modes live in system settings and power management, which is exactly where users will find them.

Get in touch and let's talk about your project.

Frequently asked questions

When should I use local notifications instead of push notifications?
Use local notifications when the trigger is knowable on the device, such as a daily reminder at 7:00 AM or a prayer time computed from the calendar. Use push notifications when the trigger lives on a server, such as a message from another user or a price alert.
Why does my daily reminder fire only once?
It was probably scheduled without a match component. Pass DateTimeComponents.time to zonedSchedule so the notification repeats at the same clock time every day.
Why is my Android notification silent or missing?
On Android 13 and later, check that notification permission was granted, and remember that a channel created with low importance stays quiet even if code later raises the importance. Use a fresh channel id during development after changing importance.

Written by Muhammad Uzair

Flutter Developer & Mobile Engineering Team Lead.

Work with me
All articles
  • Mobile Development

    8 min read

    Flutter Deep Linking: App Links Guide

    Set up verified Flutter deep links end to end: Android App Links with assetlinks.json, iOS Universal Links, and in-app routing, with working code.

  • Mobile Development

    7 min read

    Build a Health Vitals Tracker in Flutter

    Build a Flutter health vitals tracker: sub-10-second one-hand logging, per-member family profiles, offline-first storage, trend charts and daily reminders.

  • Mobile Development

    7 min read

    Control Android Ringer Mode in Flutter

    Control Android ringer mode from Flutter with a MethodChannel and AudioManager: silent, vibrate, and normal profiles plus the Do Not Disturb access step.