-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Open
Description
I’m working on a Flutter reminder app using scheduled local notifications.
Behavior I’m seeing
On emulator
-
Works in foreground
-
Works in background
-
Works in terminated state
Notifications fire exactly on time in all cases.
On real Android device
-
Works in foreground
-
Works in background
-
Unreliable in terminated state
Patterns observed on real device
-
Notifications only work when spaced far apart
-
Example:
-
8:15 AM and 8:30 AM → both fire
-
8:15 AM and 8:16 AM → one or both do not fire
-
-
Notifications scheduled very close to each other are often skipped.
-
-
Missed notifications appear when device is connected to USB
-
If some notifications are not shown,
-
When the phone is connected to USB and the app is run,
-
All previously missed notifications are shown at once.
-
android manifest file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- <uses-permission android:name="android.permission.USE_EXACT_ALARM" /> -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
<application
android:enableOnBackInvokedCallback="true"
android:label="Digital Remainder"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:showWhenLocked="true"
android:turnScreenOn="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Required to restore scheduled notifications after phone restart -->
<receiver android:exported="true" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="true" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
notification service file (initialization and schedule)
import 'package:digital_remainder/core/core.dart';
// import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
class NotificationService {
static final FlutterLocalNotificationsPlugin notificationsPlugin =
FlutterLocalNotificationsPlugin();
//notification service intialization
static Future<void> initialize() async {
const AndroidInitializationSettings androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings iosSettings =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initializationSettings =
InitializationSettings(android: androidSettings, iOS: iosSettings);
await notificationsPlugin.initialize(initializationSettings);
//initialize timezone
initializeTimeZones();
//check notification, alarm permission, and bcg optimization
await NotificationPermission.askRequiredPermissions();
}
//android and ios notification details
static Future<NotificationDetails> reminderNotificationDetails(
ReminderPriorityOptions priority,
) async {
final androidDetails = AndroidNotificationDetails(
'reminder_channel',
'Reminder Notifications',
channelDescription: 'Notifications for Digital Reminders',
importance: priority == ReminderPriorityOptions.high
? Importance.max
: priority == ReminderPriorityOptions.medium
? Importance.defaultImportance
: Importance.low,
priority: priority == ReminderPriorityOptions.high
? Priority.high
: priority == ReminderPriorityOptions.medium
? Priority.defaultPriority
: Priority.low,
playSound: true,
enableVibration: true,
// icon: '@drawable/ic_app_notification',
visibility: NotificationVisibility.public,
// sound: RawResourceAndroidNotificationSound("notification_sound"),
// color: AppColors.primary,
);
const iosDetails = DarwinNotificationDetails(
// sound: "notification_sound.wav",
presentAlert: true,
presentBadge: true,
presentSound: true,
);
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
return notificationDetails;
}
//schedule reminder notifications
static Future<void> schedule({
required String id,
required String title,
required String body,
required DateTime dateTime,
required ReminderRepeatOptions repeat,
required ReminderPriorityOptions priority,
}) async {
try {
final details = await reminderNotificationDetails(priority);
final int notifId = id.hashCode;
tz.TZDateTime scheduledDate = tz.TZDateTime.from(dateTime, tz.local);
if (scheduledDate.isBefore(tz.TZDateTime.now(tz.local))) {
return;
}
// check if exact alarms are allowed on Android
// final androidPlugin = notificationsPlugin
// .resolvePlatformSpecificImplementation<
// AndroidFlutterLocalNotificationsPlugin
// >();
// final exactAllowed =
// await androidPlugin?.requestExactAlarmsPermission() ?? false;
// AndroidScheduleMode androidScheduleMode = exactAllowed
// ? AndroidScheduleMode.alarmClock
// : AndroidScheduleMode.exactAllowWhileIdle;
AndroidScheduleMode androidScheduleMode = AndroidScheduleMode.alarmClock;
// AndroidScheduleMode androidScheduleMode =
// AndroidScheduleMode.exactAllowWhileIdle;
DateTimeComponents? matchDateTimeComponents;
switch (repeat) {
case ReminderRepeatOptions.daily:
scheduledDate = tz.TZDateTime(
tz.local,
dateTime.year,
dateTime.month,
dateTime.day,
dateTime.hour,
dateTime.minute,
);
matchDateTimeComponents = DateTimeComponents.time;
break;
case ReminderRepeatOptions.weekly:
scheduledDate = tz.TZDateTime(
tz.local,
dateTime.year,
dateTime.month,
dateTime.day,
dateTime.hour,
dateTime.minute,
);
matchDateTimeComponents = DateTimeComponents.dayOfWeekAndTime;
break;
case ReminderRepeatOptions.monthly:
scheduledDate = tz.TZDateTime(
tz.local,
dateTime.year,
dateTime.month,
dateTime.day,
dateTime.hour,
dateTime.minute,
);
matchDateTimeComponents = DateTimeComponents.dayOfMonthAndTime;
break;
default:
// matchDateTimeComponents = DateTimeComponents.dateAndTime;
matchDateTimeComponents = null;
break;
}
await notificationsPlugin.zonedSchedule(
notifId,
title,
body,
scheduledDate,
details,
androidScheduleMode: androidScheduleMode,
matchDateTimeComponents: matchDateTimeComponents,
payload: scheduledDate.toString(),
);
// debugPrint('Scheduling for: ${scheduledDate.toString()}');
// debugPrint('Now: ${tz.TZDateTime.now(tz.local)}');
// debugPrint('getting the list of set schedules:.....');
// final list = await notificationsPlugin.pendingNotificationRequests();
// debugPrint('calculating length --- :.....');
// debugPrint(list.length.toString());
// debugPrint('schedule item --- :.....');
// for (var item in list) {
// debugPrint(' --- :.....');
// debugPrint(item.title.toString());
// debugPrint(item.body.toString());
// debugPrint(item.payload.toString());
// debugPrint(' --- :.....');
// }
} catch (e) {
// print("errror: $e");
}
}
//cancel specific scheduled notification
static Future<void> cancel(String id) async {
final int notifId = id.hashCode;
await notificationsPlugin.cancel(notifId);
}
//cancel all scheduled notifications
static Future<void> cancelAll() async {
await notificationsPlugin.cancelAll();
}
//for showing instance notification : testing
static Future<void> showInstantNotification(
int id,
String? title,
String? body,
) async {
NotificationDetails notificationDetails = await reminderNotificationDetails(
ReminderPriorityOptions.high,
);
await notificationsPlugin.show(id, title, body, notificationDetails);
}
}
notification permission file:
import 'dart:io';
import 'package:digital_remainder/core/core.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:permission_handler/permission_handler.dart';
class NotificationPermission {
static final FlutterLocalNotificationsPlugin notificationsPlugin =
NotificationService.notificationsPlugin;
static Future<void> askRequiredPermissions() async {
await _askNotificationPermission();
await _askExactAlarmPermission();
await _askBatteryOptimizationPermission();
}
static Future<void> _askNotificationPermission() async {
var status = await Permission.notification.status;
if (!status.isGranted) {
var request = await Permission.notification.request();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission();
if (request.isGranted) {
CustomSnackbar.showToastMessage(
type: ToastType.success,
message: 'Notification Permission Granted.',
);
} else {
CustomSnackbar.showToastMessage(
type: ToastType.info,
message: 'Notification Permission Denied.',
);
}
}
}
static Future<void> _askExactAlarmPermission() async {
if (!Platform.isAndroid) return;
final androidPlugin = notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
final canSchedule = await androidPlugin?.canScheduleExactNotifications();
if (canSchedule == false) {
CustomSnackbar.showToastMessage(
type: ToastType.info,
message: 'Please allow Exact Alarm for reminders to work properly.',
);
await androidPlugin?.requestExactAlarmsPermission();
}
}
static Future<void> _askBatteryOptimizationPermission() async {
final status = await Permission.ignoreBatteryOptimizations.status;
if (!status.isGranted) {
var request = await Permission.ignoreBatteryOptimizations.request();
if (request.isGranted) {
CustomSnackbar.showToastMessage(
type: ToastType.success,
message: 'Battery Optimization Ignored.',
);
} else {
CustomSnackbar.showToastMessage(
type: ToastType.info,
message: 'Permission Denied.',
);
}
}
}
}
timezone initialization :
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
void initializeTimeZones() async {
final TimezoneInfo currentTimeZone = await FlutterTimezone.getLocalTimezone();
final localLocation = currentTimeZone.identifier;
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation(localLocation));
}
reminder scheduling helper:
import 'package:digital_remainder/core/core.dart';
import 'package:digital_remainder/modules/reminder/reminder.dart';
class ReminderScheduler {
static Future<void> add(ReminderEntity entity) async {
final scheduledDateTime = DateTime(
entity.date.year,
entity.date.month,
entity.date.day,
entity.time.hour,
entity.time.minute,
);
await NotificationService.schedule(
id: entity.id!,
title: entity.title,
body: entity.description ?? 'Time for this reminder',
dateTime: scheduledDateTime,
repeat: entity.repeat,
priority: entity.priority,
);
}
static Future<void> update(ReminderEntity entity) async {
await NotificationService.cancel(entity.id!);
if (entity.alertNotification) {
await add(entity);
}
}
static Future<void> delete(String id) async {
await NotificationService.cancel(id);
}
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels