-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Description
When using getNotificationAppLaunchDetails() in debug mode, the launch details persist after hot restart, which can cause confusion during development. This appears to be specific to debug mode and hot restart scenarios.
Current Behavior (Debug Mode)
- App receives notification in foreground
- App is put in background
- User taps notification from Notification Center
- App opens and processes notification using
getNotificationAppLaunchDetails() - Developer performs a hot restart
getNotificationAppLaunchDetails()still returns the previous notification details
Expected Behavior
Even in debug mode with hot restart, getNotificationAppLaunchDetails() should only return notification details when the app is launched directly from a notification tap. After a hot restart, it should return null or indicate that the app wasn't launched from a notification.
Steps to Reproduce (Debug Mode Only)
- Run app in debug mode with this notification service:
`
//code
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart';
import '../my_logger/my_logger.dart';
import 'notification_navigation.dart';
import 'setup/firebase_notification_setup.dart';
import 'setup/local_notification_setup.dart';
@singleton
class NotificationService {
final LocalNotificationSetup _localNotificationSetup =
LocalNotificationSetup();
final FirebaseNotificationSetup _firebaseNotificationSetup =
FirebaseNotificationSetup();
/// Initializes both notification systems and sets up message handlers
///
/// Used when: App starts for the first time
/// - Initializes Flutter Local Notifications for foreground messages
/// - Initializes Firebase Messaging for background/terminated messages
/// - Sets up message handlers for different app states
Future initialize() async {
logger.i('NotificationService initialize: Starting initialization');
try {
// Initialize both notification systems
await _localNotificationSetup.init(
onNotificationTap: NotificationNavigation.handleNotificationTap,
);
await _firebaseNotificationSetup.init(
onNotificationTap: NotificationNavigation.handleNotificationTap,
);
// Setup message handlers and check pending notifications
await _setupMessageHandlers();
logger.i('NotificationService initialize: Completed initialization');
} catch (e) {
logger
.e('NotificationService initialize: Error during initialization: $e');
rethrow;
}
}
/// Sets up handlers for different message scenarios and checks pending notifications
///
/// Handles three scenarios:
/// 1. Foreground messages: Uses Flutter Local Notifications
/// - When: App is open and visible
/// - Action: Shows notification using local notification system
///
/// 2. Background messages: Uses Firebase Messaging
/// - When: App is in background
/// - Action: Firebase shows system notification automatically
///
/// 3. Terminated/Launch messages: Checks both systems
/// - When: App was launched from a notification
/// - Action: Processes pending notifications from both systems
Future _setupMessageHandlers() async {
// Handle foreground messages using Flutter Local Notifications
logger.d(
'NotificationService _setupMessageHandlers: Setting up message handlers');
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
// Check for any pending notifications that might have launched the app
await _checkPendingNotifications();
}
/// Handles messages when app is in foreground
///
/// Used when: App is open and visible to user
/// System: Uses Flutter Local Notifications
/// - Receives message from Firebase
/// - Shows notification using local notification system
/// - Allows custom UI and interaction handling
void _handleForegroundMessage(RemoteMessage message) {
logger.i(
'NotificationService _handleForegroundMessage: Handling foreground message=$message');
// Show local notification when app is in foreground
_localNotificationSetup.showNotification(message);
}
/// Gets the Firebase device token for push notifications
///
/// Used when: App needs to register device for push notifications
/// System: Uses Firebase Messaging
/// - Required for sending targeted notifications to specific devices
/// - Used during user registration or token refresh
Future<String?> getDeviceToken() async {
return await _firebaseNotificationSetup.getToken();
}
/// Subscribes to a specific notification topic
///
/// Used when: App needs to receive notifications for specific topics
/// System: Uses Firebase Messaging
/// - Allows receiving notifications without storing device tokens
/// - Used for broadcast messages to specific groups
Future subscribeToTopic({required String topic}) async {
logger.d(
'NotificationService subscribeToTopic: Subscribing to topic: $topic');
try {
await FirebaseMessaging.instance.subscribeToTopic(topic);
logger.i(
'NotificationService subscribeToTopic: Successfully subscribed to events for institute with topic: $topic');
} catch (e) {
logger.e(
'NotificationService subscribeToTopic: Error subscribing to events: $e');
// rethrow;
}
}
/// Checks for any pending notification requests that might have launched the app
///
/// Used when: App is launched from a terminated state via notification
/// Systems: Checks both Firebase and Flutter Local Notifications
/// - Handles notifications that were shown while app was in foreground
/// - Handles notifications received from Firebase
Future _checkPendingNotifications() async {
logger.d(
'NotificationService _checkPendingNotifications: Checking pending notification requests');
// Check Firebase initial message first
RemoteMessage? firebaseInitial =
await FirebaseMessaging.instance.getInitialMessage();
if (firebaseInitial != null) {
logger.i(
'NotificationService _checkPendingNotifications: Found Firebase initial message: $firebaseInitial');
NotificationNavigation.handleNotificationTap(firebaseInitial);
return;
}
// Check Flutter Local Notifications pending requests
final NotificationAppLaunchDetails? launchDetails =
await _localNotificationSetup.getNotificationAppLaunchDetails();
if (launchDetails != null &&
launchDetails.didNotificationLaunchApp &&
launchDetails.notificationResponse?.payload != null) {
logger.i(
'NotificationService._checkPendingNotifications: Terminated App launched from local notification');
try {
final String payload = launchDetails.notificationResponse!.payload!;
final Map<String, dynamic> data = jsonDecode(payload);
final RemoteMessage message = RemoteMessage(data: data);
logger.i(
'NotificationService._checkPendingNotifications: Handling local notification. message=$message');
NotificationNavigation.handleNotificationTap(message);
} catch (e) {
logger.e(
'NotificationService._checkPendingNotifications: Error processing notification: $e');
}
}
// Always clear notifications at the end of checking
await _localNotificationSetup.clearNotificationDetails();
logger.d(
'NotificationService._checkPendingNotifications: Cleared all notification data');
}
}
`
`
import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../../my_logger/my_logger.dart';
import 'notification_setup_base.dart';
class LocalNotificationSetup extends NotificationSetupBase {
static final LocalNotificationSetup _instance =
LocalNotificationSetup._internal();
factory LocalNotificationSetup() => _instance;
LocalNotificationSetup._internal();
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
@OverRide
Future init({Function(RemoteMessage)? onNotificationTap}) async {
logger.d(
'LocalNotificationSetup initialize: Initializing local notifications');
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/launcher_icon');
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await _flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (response) =>
_handleNotificationResponse(response, onNotificationTap),
// onDidReceiveBackgroundNotificationResponse: (response) =>
// _handleBackgroundNotificationResponse(response, onNotificationTap),
);
}
void _handleNotificationResponse(
NotificationResponse response, Function(RemoteMessage)? onTap) {
if (response.payload != null) {
try {
final Map<String, dynamic> data = jsonDecode(response.payload!);
final RemoteMessage message = RemoteMessage(data: data);
onTap?.call(message);
} catch (e) {
logger
.e('LocalNotificationSetup _handleNotificationResponse: Error: $e');
}
}
}
@pragma('vm:entry-point')
void _handleBackgroundNotificationResponse(
NotificationResponse response, Function(RemoteMessage)? onTap) {
logger.d(
'LocalNotificationSetup _handleBackgroundNotificationResponse: Handling background notification response');
_handleNotificationResponse(response, onTap);
}
@OverRide
Future showNotification(RemoteMessage message) async {
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
if (notification != null) {
logger.i(
'LocalNotificationSetup showNotification: Showing notification. data: ${message.data}');
await _flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
'hiiCampus_channel_id',
'hiiCampus_channel_name',
channelDescription: 'Channel for hiiCampus notifications',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
fullScreenIntent: true,
visibility: NotificationVisibility.public,
channelShowBadge: true,
icon: android?.smallIcon ?? '@mipmap/launcher_icon',
styleInformation: BigTextStyleInformation(notification.body ?? ''),
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: jsonEncode(message.data),
);
}
}
@OverRide
Future<String?> getToken() async =>
null; // Not applicable for local notifications
Future<NotificationAppLaunchDetails?>
getNotificationAppLaunchDetails() async {
return await _flutterLocalNotificationsPlugin
.getNotificationAppLaunchDetails();
}
/// Clears the notification launch details to prevent reprocessing
Future clearNotificationDetails() async {
logger.d(
'LocalNotificationSetup clearNotificationDetails: Starting to clear notifications');
try {
// Cancel all notifications
await _flutterLocalNotificationsPlugin.cancelAll();
// On Android, we need to remove the notification that launched the app
if (Platform.isAndroid) {
final details = await _flutterLocalNotificationsPlugin
.getNotificationAppLaunchDetails();
if (details?.notificationResponse?.id != null) {
await _flutterLocalNotificationsPlugin
.cancel(details!.notificationResponse!.id!);
}
}
logger.i(
'LocalNotificationSetup clearNotificationDetails: Successfully cleared notifications');
// Verify clearing
final verifyDetails = await _flutterLocalNotificationsPlugin
.getNotificationAppLaunchDetails();
logger.d(
'LocalNotificationSetup clearNotificationDetails: Verification - Launch Details still present: ${verifyDetails?.didNotificationLaunchApp}');
} catch (e) {
logger.e(
'LocalNotificationSetup clearNotificationDetails: Error clearing notifications: $e');
}
}
}
`
- Send a test notification while app is in foreground
- Background the app
- Tap the notification to open the app
- Verify notification details are processed
- Perform a hot restart
- Check
getNotificationAppLaunchDetails()- it still returns the previous notification details
Important Notes
- This issue only occurs in debug mode
- The behavior is specifically related to hot restart
- In production builds, this behavior should not occur as we can't do hot-restart
- This can cause confusion during development and testing
Environment
- flutter_local_notifications version: ^18.0.1
- Flutter version: 3.27.1
- Platform: Checked in Android
- Debug Mode: Yes
- Reproduction Method: Hot Restart
Additional Context
We've attempted to clear the notification details using:
cancelAll()cancel
However, the launch details still persist across hot restarts in debug mode.
Impact
While this shouldn't affect production builds, it makes development and testing of notification-related features more difficult as developers need to fully terminate the app to get accurate notification launch behavior.
Possible Solutions
- Clear launch details after hot restart in debug mode
- Add a development-time flag to force clear launch details
- Add documentation noting this behavior in debug mode
- Add a method to explicitly clear launch details that works in debug mode