Skip to content

Flutter zoned scheduled notifications work in foreground/background but unreliable in terminated state on real Android device #2737

@sudip22-p

Description

@sudip22-p

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

  1. 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.

  2. 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);
  }
}


Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions