Skip to content
Blog

Flutter Local Notifications: Scheduling, Permissions, and Tap Handling

A practical guide to flutter_local_notifications — initialization, Android 13 permissions, zonedSchedule with timezones, background tap callbacks, and platform limits.

Published on • October 9, 2026

AI Assistant

Local notifications are generated on the device — reminders, scheduled alerts, in-app events — with no server in the loop. They complement push notifications (FCM/APNs), which arrive from a server. The standard Flutter package for them is flutter_local_notifications, currently at v22.x and a Flutter Favorite.

Here is what it takes to ship them correctly.

Platform setup

Android — Gradle (required even if you never schedule):

android {
    compileOptions {
        coreLibraryDesugaringEnabled true
    }
    dependencies {
        coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
    }
}

Desugaring is required from v10+ because the plugin uses java.time APIs. You also need Java 17 and compileSdk 35+.

Android — manifest receivers for scheduling:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- plus the plugin's ScheduledNotificationReceiver and ScheduledNotificationBootReceiver -->

Without the boot receivers, scheduled notifications vanish after a reboot.

iOS — AppDelegate:

UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate

Note: the APNs entitlement is only needed for remote push. Pure local notifications require no APNs setup.

Initialization

All platform settings are nullable — but forgetting the one for a platform you target throws an ArgumentError:

const android = AndroidInitializationSettings('app_icon');
const darwin = DarwinInitializationSettings();
final settings = InitializationSettings(android: android, iOS: darwin, macOS: darwin);

await flutterLocalNotificationsPlugin.initialize(
  settings,
  onDidReceiveNotificationResponse: (response) async {
    await Navigator.push(context, MaterialPageRoute(
      builder: (_) => SecondScreen(response.payload),
    ));
  },
  onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);

Permissions

Android 13+ (API 33) requires a runtime grant for POST_NOTIFICATIONS:

flutterLocalNotificationsPlugin
    .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
    ?.requestNotificationsPermission();

Android 12+ exact alarms need either SCHEDULE_EXACT_ALARM plus requestExactAlarmsPermission(), or USE_EXACT_ALARM (no prompt, but subject to store review). Without it, AndroidScheduleMode.exactAllowWhileIdle logs an error and recurring notifications will not reschedule.

iOS permission is requested via resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()?.requestPermissions(alert: true, badge: true, sound: true), or deferred by setting the DarwinInitializationSettings request flags to false and prompting at a moment of your choosing.

Scheduling with timezones

Since plugin v2.0, scheduling uses zonedSchedule with TZDateTime from the timezone package — this fixed the daylight-saving bugs of the deprecated schedule():

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

tz.initializeTimeZones();
// Device timezone is not built in — resolve it with flutter_timezone
tz.setLocalLocation(tz.getLocation(timeZoneName));

await flutterLocalNotificationsPlugin.zonedSchedule(
  0,
  'Daily standup',
  'Starts in 15 minutes',
  tz.TZDateTime.now(tz.local).add(const Duration(minutes: 15)),
  const NotificationDetails(
    android: AndroidNotificationDetails(
      'reminders',
      'Reminders',
      channelDescription: 'Scheduled reminders',
    ),
  ),
  androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
);

For recurring notifications, pass matchDateTimeComponents: DateTimeComponents.time (daily) or .dayOfWeekAndTime (weekly) instead of computing the next occurrence yourself. periodicallyShow() covers simple repeats but is not supported on Windows.

Three distinct callbacks:

CallbackWhen it fires
onDidReceiveNotificationResponseForeground tap
onDidReceiveBackgroundNotificationResponseBackground/terminated action tap
getNotificationAppLaunchDetails()Cold start from a notification

The background callback must be a top-level or static function annotated with @pragma('vm:entry-point') — it runs on a separate Flutter engine:

@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse response) {
  // runs outside your app's main isolate
}

Pass a payload: when showing a notification and read it back from NotificationResponse.payload. For cold starts, onDidReceiveNotificationResponse cannot handle launch — use:

final details = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (details?.didNotificationLaunchApp ?? false) {
  final payload = details!.notificationAppLaunchDetails!.notificationResponse!.payload;
  // route from payload
}

Android 12+ trampoline restriction

Apps targeting Android 12+ cannot start activities from a Service or BroadcastReceiver used as a notification trampoline — taps must launch the activity directly. In practice this means startActivity() from a background action callback gets blocked with “Indirect notification activity start (trampoline) blocked.” The plugin’s maintainers treat this as an Android-level constraint requiring your own custom solution, so test action taps on a real Android 12+ device.

Platform limits worth memorizing

  • Android channels: sound/vibration are fixed at channel creation. Re-specifying them on the same channel ID does nothing.
  • iOS: only the 64 most recently scheduled notifications are kept.
  • OEM battery killers (Xiaomi, Huawei) can drop background scheduling — not fixable from the plugin.
  • Windows: no repeating notifications; cancel/getActiveNotifications need MSIX packaging.
  • Linux: no scheduling API at all.
  • Web: permission must be requested from a user gesture; no scheduled notifications.
  • ProGuard/R8: keep notification icons via keep.xml or they silently vanish in release builds.
  • Coexistence with firebase_messaging works on 6.0.13+; older versions stomped each other’s callbacks.

Local vs. push: when to use which

LocalPush (FCM/APNs)
SourceOn-deviceServer
NetworkNot neededNeeded
Use casesReminders, timers, scheduled contentMarketing, chat, out-of-band events
SetupOne packageServer + credentials + permissions

Most real apps use both: local for time-based on-device events, push for anything the server knows about first.

Wrapping up

The plugin handles the hard platform matrix, but correctness lives in the details: desugaring in Gradle, the boot receivers, the Android 13 permission prompt, timezone-aware zonedSchedule, the vm:entry-point background callback, and getNotificationAppLaunchDetails for cold starts. Wire those six things up and test taps on a physical device before release.

References