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.
Tap handling and deep links
Three distinct callbacks:
| Callback | When it fires |
|---|---|
onDidReceiveNotificationResponse | Foreground tap |
onDidReceiveBackgroundNotificationResponse | Background/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/getActiveNotificationsneed 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.xmlor they silently vanish in release builds. - Coexistence with
firebase_messagingworks on 6.0.13+; older versions stomped each other’s callbacks.
Local vs. push: when to use which
| Local | Push (FCM/APNs) | |
|---|---|---|
| Source | On-device | Server |
| Network | Not needed | Needed |
| Use cases | Reminders, timers, scheduled content | Marketing, chat, out-of-band events |
| Setup | One package | Server + 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
- flutter_local_notifications on pub.dev - full README, platform settings, and scheduling API
- Plugin example - complete working example across platforms
- Android notification trampoline restrictions - why background activity starts are blocked