Skip to content

1. Usage 10.0.0 .Net MAUI

Elvin Thudugala edited this page Mar 29, 2026 · 17 revisions

.NET MAUI Local Notifications Guide

Installation

Packages

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

Installation Options

1. Using NuGet Package Manager UI

  1. Open your solution in Visual Studio
  2. Right-click on your project
  3. Select "Manage NuGet Packages"
  4. Search for "Plugin.LocalNotification"
  5. Click Install
  6. Optionally install "Plugin.LocalNotification.Geofence" for location-based notifications

2. Using .NET CLI

dotnet add package Plugin.LocalNotification
# Optional: for geofence/location-based notifications
dotnet add package Plugin.LocalNotification.Geofence

3. Using Package Manager Console

Install-Package Plugin.LocalNotification
# Optional: for geofence/location-based notifications
Install-Package Plugin.LocalNotification.Geofence

The plugin requires .NET 10 MAUI and supports Android, iOS, MacCatalyst, and Windows platforms.

Key Namespaces

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)

Platform Configuration

Android Setup

Required Permissions

Add the following permissions either through assembly attributes or in the Android Manifest.

Option 1: Assembly Attributes

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 permission
Option 2: Android Manifest

In 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" />

iOS Setup

Add the following to Info.plist:

<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
    <string>remote-notification</string>
</array>

Application Setup

Basic Initialization

In MauiProgram.cs:

using Plugin.LocalNotification;

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .UseLocalNotification(); // Add this line

    return builder.Build();
}

Advanced Configuration

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();
}

Permission Management and Verification

Basic Permission Request

// Check if notifications are enabled
if (await LocalNotificationCenter.Current.AreNotificationsEnabled() == false)
{
    // Basic permission request
    await LocalNotificationCenter.Current.RequestNotificationPermission();
}

Advanced Permission Configuration

// 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}");
}

Permission Best Practices

Timing and User Experience

  • 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;
    }
}

Graceful Degradation

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();
        }
    }
}

Setup Verification

// 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);

Granular Permission Status

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

Do Not Disturb Bypass

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() and RequestNotificationPolicyAccess() are no-ops on Android versions below 6.0 (API 23) and always return true.
CanBypassDnd on a channel is silently ignored by the OS until policy access is granted.

Active Notifications with Rich Metadata

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.

Usage Examples

Basic Notification

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);

Scheduled Notification

Note: NotificationRequestSchedule.NotifyTime and NotifyAutoCancelTime are DateTimeOffset?. Use DateTimeOffset.Now (not DateTime.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 Rich Notification Styles

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.

Handling Notification Events

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;
        }
    }
}

App Launch Detection

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, LaunchNotificationDetails is set when UNUserNotificationCenterDelegate.DidReceiveNotificationResponse is called immediately after FinishedLaunching. If no notification response arrives (i.e. the app was opened normally), DidNotificationLaunchApp is set to false when WillEnterForeground fires.

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;
        }
    }
}

MVVM Pattern Support

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"
        });
    }
}

Additional Features

For more advanced features, refer to:

Troubleshooting

Android Issues

  1. Missing Permissions

    • Verify all required permissions are added
    • Check runtime permissions for Android 13+
    • Ensure battery optimization settings aren't restricting your app
  2. Channel Not Found

    • Ensure notification channels are created
    • Check channel IDs match
    • Verify channel configuration matches your requirements

iOS Issues

  1. Notifications Not Showing

    • Verify Info.plist configuration
    • Check notification permissions are granted
    • Ensure proper provisioning profile setup
  2. Background Issues

    • Confirm background modes are enabled
    • Check background fetch configuration
    • Verify remote-notification capability

Build Issues

  1. Package Installation

    • Clean and rebuild your solution
    • Check for package version conflicts
    • Verify target framework compatibility
  2. 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.

Clone this wiki locally