-
-
Notifications
You must be signed in to change notification settings - Fork 72
1. Usage 10.0.0 .Net MAUI
The plugin is split into three NuGet packages:
| Package | Purpose |
|---|---|
Plugin.LocalNotification |
Main notification service (required) |
Plugin.LocalNotification.Core |
Core types and interfaces (automatically included) |
Plugin.LocalNotification.Geofence |
Optional geofence/location-based notification support |
- Open your solution in Visual Studio
- Right-click on your project
- Select "Manage NuGet Packages"
- Search for "Plugin.LocalNotification"
- Click Install
- Optionally install "Plugin.LocalNotification.Geofence" for location-based notifications
dotnet add package Plugin.LocalNotification
# Optional: for geofence/location-based notifications
dotnet add package Plugin.LocalNotification.GeofenceInstall-Package Plugin.LocalNotification
# Optional: for geofence/location-based notifications
Install-Package Plugin.LocalNotification.GeofenceThe plugin requires .NET 10 MAUI and supports Android, iOS, MacCatalyst, and Windows platforms.
using Plugin.LocalNotification; // Main API (LocalNotificationCenter, INotificationService)
using Plugin.LocalNotification.Core; // Logging (LocalNotificationLogger)
using Plugin.LocalNotification.Core.Models; // Models (NotificationRequest, NotificationCategory, etc.)
using Plugin.LocalNotification.Core.Models.AndroidOption; // Android-specific models (AndroidOptions, AndroidNotificationChannelRequest, etc.)
using Plugin.LocalNotification.Core.Models.AppleOption; // Apple (iOS/MacCatalyst) models (AppleOptions, AppleActionType, etc.)
using Plugin.LocalNotification.Geofence; // Geofence extension (UseLocalNotificationGeofence)Add the following permissions either through assembly attributes or in the Android Manifest.
In Platforms/Android/MainApplication.cs, add:
// Essential permissions
[assembly: UsesPermission(Manifest.Permission.Vibrate)]
[assembly: UsesPermission(Manifest.Permission.POST_NOTIFICATIONS)]
[assembly: UsesPermission(Manifest.Permission.WAKE_LOCK)]
[assembly: UsesPermission(Manifest.Permission.RECEIVE_BOOT_COMPLETED)]
// Optional: For exact timing in Calendar/Alarm apps (Android 14+)
[assembly: UsesPermission("android.permission.USE_EXACT_ALARM")] // No user prompt, requires Android 13+
[assembly: UsesPermission("android.permission.SCHEDULE_EXACT_ALARM")] // User can grant permissionIn Platforms/Android/AndroidManifest.xml, add:
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Optional: For exact timing -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" android:maxSdkVersion="32" />Add the following to Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>In MauiProgram.cs:
using Plugin.LocalNotification;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseLocalNotification(); // Add this line
return builder.Build();
}using Plugin.LocalNotification;
using Plugin.LocalNotification.Core.Models;
using Plugin.LocalNotification.Core.Models.AndroidOption;
using Plugin.LocalNotification.Core.Models.AppleOption;
using Plugin.LocalNotification.Geofence; // Only if using geofence
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseLocalNotification(config =>
{
// Configure Android channels
config.AddAndroid(android =>
{
android.AddChannel(new AndroidNotificationChannelRequest
{
Id = "general",
Name = "General",
Description = "General notifications"
});
});
// Configure iOS settings
config.AddiOS(ios =>
{
#if IOS
ios.SetCustomUserNotificationCenterDelegate(
new CustomUserNotificationCenterDelegate()
);
#endif
});
// Configure serialisation if needed
config.SetSerializer(new MyCustomSerializer());
})
// Optional: Enable geofence support (requires Plugin.LocalNotification.Geofence package)
.UseLocalNotificationGeofence();
return builder.Build();
}// Check if notifications are enabled
if (await LocalNotificationCenter.Current.AreNotificationsEnabled() == false)
{
// Basic permission request
await LocalNotificationCenter.Current.RequestNotificationPermission();
}// Request with specific options
var permissionRequest = new NotificationPermission
{
// Apple (iOS/MacCatalyst) settings
Apple =
{
// Configure authorisation options
NotificationAuthorization = AppleAuthorizationOptions.Alert |
AppleAuthorizationOptions.Badge |
AppleAuthorizationOptions.Sound,
// Configure location authorisation (if using geofencing)
LocationAuthorization = AppleLocationAuthorization.Always
},
// Android-specific settings
Android =
{
// indicating whether to request permission to schedule exact alarms (for scheduled notifications)
RequestPermissionToScheduleExactAlarm = true
}
};
try
{
var result = await LocalNotificationCenter.Current.RequestNotificationPermission(permissionRequest);
if (!result)
{
// Handle permission denial
Debug.WriteLine("Notification permissions denied");
// Optionally guide user to settings
if (DeviceInfo.Platform == DevicePlatform.iOS)
{
await Launcher.OpenAsync("app-settings:");
}
else if (DeviceInfo.Platform == DevicePlatform.Android)
{
await Launcher.OpenAsync(new Uri("package:com.yourapp.package"));
}
}
}
catch (Exception ex)
{
// Handle permission request errors
Debug.WriteLine($"Permission request failed: {ex.Message}");
}- Request permissions at appropriate moments
- Explain why permissions are needed
- Consider using a "soft ask" approach
public class NotificationPermissionManager
{
private readonly IPreferences _preferences;
public async Task<bool> RequestPermissionsWithSoftAsk()
{
// Check if we've shown the soft ask
if (!_preferences.Get("has_shown_notification_prompt", false))
{
// Show custom UI explaining why notifications are needed
var shouldRequest = await ShowCustomPermissionDialog();
if (!shouldRequest)
{
_preferences.Set("has_shown_notification_prompt", true);
return false;
}
}
// Proceed with actual permission request
var result = await LocalNotificationCenter.Current.RequestNotificationPermission();
return result;
}
}public async Task HandlePermissionDenial()
{
// Check if permissions can be requested again
var status = await LocalNotificationCenter.Current.AreNotificationsEnabled();
if (!status)
{
// Show dialogue explaining impact
var shouldOpenSettings = await ShowPermissionDeniedDialog();
if (shouldOpenSettings)
{
// Guide user to app settings
await OpenAppSettings();
}
else
{
// Enable alternative features
EnableAlternativeFeatures();
}
}
}// Send a test notification
var notification = new NotificationRequest
{
NotificationId = 1,
Title = "Setup Test",
Description = "If you see this, setup was successful!"
};
await LocalNotificationCenter.Current.Show(notification);AreNotificationsEnabled() returns a single boolean. When you need a finer breakdown — for example
to explain to the user exactly which capabilities are disabled — call GetNotificationPermissionStatus().
var status = await LocalNotificationCenter.Current.GetNotificationPermissionStatus();
if (!status.IsEnabled)
{
// Notifications are completely disabled — prompt the user to enable them in Settings.
}
else
{
if (!status.IsSoundEnabled)
{
Debug.WriteLine("Notification sounds are disabled. Users may miss alerts.");
}
if (!status.IsBadgeEnabled)
{
Debug.WriteLine("Badge updates are disabled.");
}
if (!status.IsCriticalAlertEnabled)
{
// iOS/MacCatalyst only — critical alerts bypass Do Not Disturb.
Debug.WriteLine("Critical alerts are not granted.");
}
if (!status.IsTimeSensitiveEnabled)
{
// iOS 15+ / macOS 12+ only — time-sensitive notifications break through Focus modes.
Debug.WriteLine("Time-sensitive notifications are not allowed.");
}
}
// Android: check whether exact alarms are permitted (Android 12+ / API 31+).
if (!status.CanScheduleExactAlarms)
{
// Guide the user to grant the SCHEDULE_EXACT_ALARM permission.
if (LocalNotificationCenter.AndroidService is { } androidService)
{
await androidService.RequestExactAlarmsPermission();
}
}Property reference:
| Property | Android | iOS / MacCatalyst | Windows |
|---|---|---|---|
IsEnabled |
NotificationManager.AreNotificationsEnabled() |
AuthorizationStatus is Authorized or Provisional |
AppNotificationManager.Setting == Enabled |
IsAlertEnabled |
same as IsEnabled
|
UNNotificationSettings.AlertSetting |
same as IsEnabled
|
IsSoundEnabled |
same as IsEnabled
|
UNNotificationSettings.SoundSetting |
same as IsEnabled
|
IsBadgeEnabled |
same as IsEnabled
|
UNNotificationSettings.BadgeSetting |
same as IsEnabled
|
IsProvisionalEnabled |
always false
|
AuthorizationStatus == Provisional |
always false
|
IsCriticalAlertEnabled |
always false
|
UNNotificationSettings.CriticalAlertSetting |
always false
|
IsCarPlayEnabled |
always false
|
UNNotificationSettings.CarPlaySetting |
always false
|
IsTimeSensitiveEnabled |
always false
|
UNNotificationSettings.TimeSensitiveSetting (iOS 15+) |
always false
|
CanScheduleExactAlarms |
AlarmManager.CanScheduleExactAlarms() (API 31+), true on older versions |
always true
|
always true
|
Android notification channels support a CanBypassDnd flag that lets a channel's notifications ring even
when the device is in Do Not Disturb mode. However the OS requires the app to hold Notification Policy
Access before this flag takes effect.
Use IAndroidNotificationService to check and request the necessary permission:
if (LocalNotificationCenter.AndroidService is { } androidService)
{
bool hasAccess = await androidService.HasNotificationPolicyAccess();
if (!hasAccess)
{
// Opens system Notification Policy Access settings so the user can grant permission.
hasAccess = await androidService.RequestNotificationPolicyAccess();
}
if (hasAccess)
{
// Now create (or re-create) the channel with CanBypassDnd = true.
await androidService.DeleteNotificationChannel("critical-alerts");
await LocalNotificationCenter.CreateAndroidNotificationChannel(new AndroidNotificationChannelRequest
{
ChannelId = "critical-alerts",
ChannelName = "Critical Alerts",
Description = "Urgent alerts that break through Do Not Disturb",
Importance = AndroidImportance.Max,
CanBypassDnd = true
});
}
}Note:
HasNotificationPolicyAccess()andRequestNotificationPolicyAccess()are no-ops on Android versions below 6.0 (API 23) and always returntrue.
CanBypassDndon a channel is silently ignored by the OS until policy access is granted.
GetActiveNotifications() returns the notifications currently visible in the device's notification center,
with richer metadata than the internal GetDeliveredNotificationList() which relies on stored data.
| Property | Android | iOS / macOS | Windows |
|---|---|---|---|
NotificationId |
StatusBarNotification.Id |
parsed from UNNotificationRequest.Identifier
|
Not supported |
Title |
Notification.Extras["android.title"] |
UNNotificationContent.Title |
Not supported |
Body |
Notification.Extras["android.text"] |
UNNotificationContent.Body |
Not supported |
ChannelId |
Notification.ChannelId (API 26+) |
always null
|
always null
|
Tag |
StatusBarNotification.Tag |
always null
|
always null
|
GroupKey |
Notification.Group |
UNNotificationContent.ThreadIdentifier |
always null
|
BigText |
Notification.Extras["android.bigText"] |
always null
|
always null
|
Payload |
always null
|
NotificationRequest.ReturningData from UserInfo
|
always null
|
var activeNotifications = await LocalNotificationCenter.Current.GetActiveNotifications();
foreach (var notification in activeNotifications)
{
Debug.WriteLine($"[{notification.NotificationId}] {notification.Title}: {notification.Body}");
if (notification.BigText is not null)
{
// Android only — the expanded BigText style body
Debug.WriteLine($" BigText: {notification.BigText}");
}
if (notification.Payload is not null)
{
// iOS/macOS only — the ReturningData payload from the original NotificationRequest
Debug.WriteLine($" Payload: {notification.Payload}");
}
}Note: On Android,
GetActiveNotifications()requires Android 6.0+ (API 23) and returns an empty list on older versions. On Windows, an empty list is always returned because Windows does not expose an API for querying currently displayed notifications.
using Plugin.LocalNotification;
using Plugin.LocalNotification.Core.Models;
// Check/Request Permissions
if (await LocalNotificationCenter.Current.AreNotificationsEnabled() == false)
{
await LocalNotificationCenter.Current.RequestNotificationPermission();
}
// Create and show notification
var notification = new NotificationRequest
{
NotificationId = 100,
Title = "Notification Title",
Description = "This is the notification message",
ReturningData = "custom-data" // Data to return when notification is tapped
};
await LocalNotificationCenter.Current.Show(notification);Note:
NotificationRequestSchedule.NotifyTimeandNotifyAutoCancelTimeareDateTimeOffset?. UseDateTimeOffset.Now(notDateTime.Now) when constructing scheduled requests so that timezone information is preserved correctly.
var scheduledNotification = new NotificationRequest
{
NotificationId = 101,
Title = "Reminder",
Description = "Don't forget your appointment!",
Schedule =
{
NotifyTime = DateTimeOffset.Now.AddHours(2) // Schedule 2 hours from now
}
};
await LocalNotificationCenter.Current.Show(scheduledNotification);Android supports several expanded notification layouts. Set AndroidOptions.Style to select one.
// Inbox style — multiple summary lines
var inboxNotification = new NotificationRequest
{
NotificationId = 102,
Title = "3 new emails",
Android =
{
Style = new AndroidInboxStyle
{
SummaryText = "inbox@example.com",
Lines = ["Alice: Catch up soon?", "Bob: Meeting at 3pm", "Carol: Report attached"]
}
}
};
// Messaging style — conversation thread
var messagingNotification = new NotificationRequest
{
NotificationId = 103,
Title = "Team Chat",
Android =
{
Style = new AndroidMessagingStyle
{
Person = new AndroidStylePerson { Name = "Me" },
IsGroupConversation = true,
Messages =
[
new AndroidStyleMessage
{
Text = "Ready for stand-up?",
Person = new AndroidStylePerson { Name = "Alice" },
Timestamp = DateTimeOffset.Now.AddMinutes(-2)
}
]
}
}
};See the full Android Notification Styles guide for Messaging, Media, Chronometer, and Colorized options.
In your App.xaml.cs:
public partial class App : Application
{
public App()
{
InitializeComponent();
// Subscribe to notification events
LocalNotificationCenter.Current.NotificationActionTapped += OnNotificationActionTapped;
MainPage = new MainPage();
}
private void OnNotificationActionTapped(NotificationActionEventArgs e)
{
if (e.IsDismissed)
{
// Handle notification dismissal
return;
}
if (e.IsTapped)
{
// Handle notification tap
var returnedData = e.Request.ReturningData;
// Process the returned data
return;
}
// Handle notification actions if configured
switch (e.ActionId)
{
case 1:
// Handle action 1
break;
case 2:
// Handle action 2
break;
}
}
}You can determine whether the application was cold-started by the user tapping a notification. This is useful for deep-linking to a specific page on startup.
LocalNotificationCenter.LaunchNotificationDetails is populated during the platform lifecycle
events registered by UseLocalNotification(). Query it after the app has finished launching —
for example in your main page constructor or App.xaml.cs.
var launchDetails = LocalNotificationCenter.LaunchNotificationDetails;
if (launchDetails?.DidNotificationLaunchApp == true)
{
// The app was cold-started from a notification tap.
var request = launchDetails.Request; // The original NotificationRequest
var actionId = launchDetails.ActionId; // Which action was tapped
if (!string.IsNullOrEmpty(request?.ReturningData))
{
// Parse ReturningData and navigate to the appropriate page
// e.g. await Shell.Current.GoToAsync(nameof(DetailPage));
}
}Note: On iOS and MacCatalyst,
LaunchNotificationDetailsis set whenUNUserNotificationCenterDelegate.DidReceiveNotificationResponseis called immediately afterFinishedLaunching. If no notification response arrives (i.e. the app was opened normally),DidNotificationLaunchAppis set tofalsewhenWillEnterForegroundfires.
public partial class App : Application
{
public App()
{
InitializeComponent();
// Subscribe to notification events
LocalNotificationCenter.Current.NotificationActionTapped += OnNotificationActionTapped;
MainPage = new MainPage();
}
private void OnNotificationActionTapped(NotificationActionEventArgs e)
{
if (e.IsDismissed)
{
// Handle notification dismissal
return;
}
if (e.IsTapped)
{
// Handle notification tap
var returnedData = e.Request.ReturningData;
// Process the returned data
return;
}
// Handle notification actions if configured
switch (e.ActionId)
{
case 1:
// Handle action 1
break;
case 2:
// Handle action 2
break;
}
}
}The plugin supports dependency injection. Instead of using LocalNotificationCenter.Current, you can inject INotificationService:
public class YourViewModel
{
private readonly INotificationService _notificationService;
public YourViewModel(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task ShowNotification()
{
await _notificationService.Show(new NotificationRequest
{
NotificationId = 100,
Title = "MVVM Notification",
Description = "This notification was triggered from a ViewModel"
});
}
}For more advanced features, refer to:
- Notification Actions
- Scheduled Notifications
- Repeat Notifications
- Custom Sounds
- Location-Based Notifications (Geofence)
- Android Notification Styles
- Android Foreground Service
-
Missing Permissions
- Verify all required permissions are added
- Check runtime permissions for Android 13+
- Ensure battery optimization settings aren't restricting your app
-
Channel Not Found
- Ensure notification channels are created
- Check channel IDs match
- Verify channel configuration matches your requirements
-
Notifications Not Showing
- Verify Info.plist configuration
- Check notification permissions are granted
- Ensure proper provisioning profile setup
-
Background Issues
- Confirm background modes are enabled
- Check background fetch configuration
- Verify remote-notification capability
-
Package Installation
- Clean and rebuild your solution
- Check for package version conflicts
- Verify target framework compatibility
-
Runtime Errors
- Enable detailed logging for debugging
- Check application output for error messages
- Verify all platform-specific setup steps
For more troubleshooting tips and standard solutions, see the FAQ.