
Maui Local Notifications
- 33 installs
- 163 repo stars
- Updated July 6, 2026
- davidortinau/maui-skills
Adds local notifications to .NET MAUI apps on Android, iOS, and Mac Catalyst, covering channels, permissions, scheduling, and foreground/background handling.
About
Guides adding local notifications to .NET MAUI apps on Android, iOS and Mac Catalyst, covering channels, permissions, scheduling and foreground/background handling. A developer uses it when scheduling on-device notifications in a MAUI app.
- Notification channels, permissions, and scheduling
- Foreground and background handling across platforms
Maui Local Notifications by the numbers
- 33 all-time installs (skills.sh)
- Ranked #648 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-local-notificationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 163 |
| Last updated | July 6, 2026 |
| Repository | davidortinau/maui-skills ↗ |
What it does
Adds local notifications to .NET MAUI apps on Android, iOS, and Mac Catalyst, covering channels, permissions, scheduling, and foreground/background handling.
Files
.NET MAUI Local Notifications
Implementation overview
1. Define cross-platform INotificationManagerService interface and event args 2. Implement Android notification service (channel, AlarmManager, BroadcastReceiver) 3. Implement iOS/Mac Catalyst notification service (UNUserNotificationCenter) 4. Register platform implementations via DI 5. Configure platform permissions and MainActivity
See references/local-notifications-api.md for full implementation code.
Platform gotchas
Android
| Issue | Fix |
|---|---|
| Notifications silently fail on API 33+ | Must request POST_NOTIFICATIONS runtime permission first |
PendingIntent crash on Android 12+ | Must include PendingIntentFlags.Immutable for API 31+ |
| Scheduled notifications lost on reboot | AlarmManager does not survive device restart — re-schedule on boot via BOOT_COMPLETED receiver |
| No notification appears | Channel not created — required on API 26+ (Android 8.0) |
| Notification tap doesn't return to app | LaunchMode = LaunchMode.SingleTop must be set on MainActivity |
// ✅ Correct — API 31+ requires Immutable flag
var pendingIntentFlags = (Build.VERSION.SdkInt >= BuildVersionCodes.S)
? PendingIntentFlags.CancelCurrent | PendingIntentFlags.Immutable
: PendingIntentFlags.CancelCurrent;
// ❌ Wrong — crashes on Android 12+
var pendingIntentFlags = PendingIntentFlags.CancelCurrent;iOS / Mac Catalyst
| Issue | Fix |
|---|---|
| No notification prompt appears | Permission already denied — user must re-enable in Settings |
| Foreground notifications don't show | Must implement UNUserNotificationCenterDelegate and set Current.Delegate |
Notification shows Alert not Banner | Use UNNotificationPresentationOptions.Banner on iOS 14+ |
// ✅ iOS 14+ — use Banner
completionHandler(OperatingSystem.IsIOSVersionAtLeast(14)
? UNNotificationPresentationOptions.Banner
: UNNotificationPresentationOptions.Alert);
// ❌ Always using Alert — deprecated on iOS 14+
completionHandler(UNNotificationPresentationOptions.Alert);Windows
⚠️ Windows App SDK supports toast notifications but scheduled notifications are not yet supported. Immediate notifications work.
DI registration — platform-specific only
// ✅ Must use #if guards — there's no cross-platform implementation
#if ANDROID
builder.Services.AddTransient<INotificationManagerService,
Platforms.Android.NotificationManagerService>();
#elif IOS
builder.Services.AddTransient<INotificationManagerService,
Platforms.iOS.NotificationManagerService>();
#elif MACCATALYST
builder.Services.AddTransient<INotificationManagerService,
Platforms.MacCatalyst.NotificationManagerService>();
#endifCommon anti-patterns
// ❌ Sending without checking permission — silent failure on API 33+
notificationManager.SendNotification("Title", "Body");
// ✅ Request permission first
#if ANDROID
var status = await Permissions.RequestAsync<Platforms.Android.NotificationPermission>();
if (status != PermissionStatus.Granted) return;
#endif
// ❌ Updating UI directly from notification callback — cross-thread exception
notificationManager.NotificationReceived += (s, e) =>
myLabel.Text = ((NotificationEventArgs)e).Title;
// ✅ Marshal to UI thread
notificationManager.NotificationReceived += (s, e) =>
MainThread.BeginInvokeOnMainThread(() =>
myLabel.Text = ((NotificationEventArgs)e).Title);Decision framework
| Need | Approach |
|---|---|
| Immediate notification | SendNotification(title, message) with null notifyTime |
| Scheduled reminder | SendNotification(title, message, DateTime.Now.AddMinutes(30)) |
| Persist across reboot (Android) | Add BOOT_COMPLETED receiver to re-schedule alarms |
| Rich notifications (images, actions) | Extend platform implementations with native APIs |
| Push notifications from server | Use a different pattern entirely (FCM/APNs) |
Quick checklist
- [ ] Cross-platform
INotificationManagerServiceinterface defined - [ ] Android: notification channel created (API 26+)
- [ ] Android:
POST_NOTIFICATIONSpermission in manifest + runtime request (API 33+) - [ ] Android:
PendingIntentFlags.Immutableused (API 31+) - [ ] Android:
MainActivityhasLaunchMode.SingleTopand handlesOnNewIntent - [ ] iOS:
UNUserNotificationCenterDelegateset for foreground display - [ ] iOS:
Bannerused instead ofAlerton iOS 14+ - [ ] DI registration uses
#ifplatform guards - [ ] UI updates from notification callbacks use
MainThread.BeginInvokeOnMainThread
Local Notifications Implementation Reference
Cross-Platform Interface
Create in shared project:
public class NotificationEventArgs : EventArgs
{
public string Title { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
public interface INotificationManagerService
{
event EventHandler NotificationReceived;
void SendNotification(string title, string message, DateTime? notifyTime = null);
void ReceiveNotification(string title, string message);
}Android Implementation
Place in Platforms/Android/:
NotificationManagerService.cs
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using AndroidX.Core.App;
namespace YOUR_NAMESPACE.Platforms.Android;
public class NotificationManagerService : INotificationManagerService
{
const string channelId = "default";
const string channelName = "Default";
const string channelDescription = "The default channel for notifications.";
public const string TitleKey = "title";
public const string MessageKey = "message";
bool channelInitialized = false;
int messageId = 0;
int pendingIntentId = 0;
NotificationManagerCompat compatManager;
public event EventHandler NotificationReceived;
public static NotificationManagerService Instance { get; private set; }
public NotificationManagerService()
{
if (Instance == null)
{
CreateNotificationChannel();
compatManager = NotificationManagerCompat.From(Platform.AppContext);
Instance = this;
}
}
public void SendNotification(string title, string message, DateTime? notifyTime = null)
{
if (!channelInitialized)
CreateNotificationChannel();
if (notifyTime != null)
{
var intent = new Intent(Platform.AppContext, typeof(AlarmHandler));
intent.PutExtra(TitleKey, title);
intent.PutExtra(MessageKey, message);
intent.SetFlags(ActivityFlags.SingleTop | ActivityFlags.ClearTop);
var pendingIntentFlags = (Build.VERSION.SdkInt >= BuildVersionCodes.S)
? PendingIntentFlags.CancelCurrent | PendingIntentFlags.Immutable
: PendingIntentFlags.CancelCurrent;
var pendingIntent = PendingIntent.GetBroadcast(Platform.AppContext, pendingIntentId++, intent, pendingIntentFlags);
long triggerTime = GetNotifyTime(notifyTime.Value);
var alarmManager = Platform.AppContext.GetSystemService(Context.AlarmService) as AlarmManager;
alarmManager.Set(AlarmType.RtcWakeup, triggerTime, pendingIntent);
}
else
{
Show(title, message);
}
}
public void ReceiveNotification(string title, string message)
{
NotificationReceived?.Invoke(null, new NotificationEventArgs { Title = title, Message = message });
}
public void Show(string title, string message)
{
var intent = new Intent(Platform.AppContext, typeof(MainActivity));
intent.PutExtra(TitleKey, title);
intent.PutExtra(MessageKey, message);
intent.SetFlags(ActivityFlags.SingleTop | ActivityFlags.ClearTop);
var pendingIntentFlags = (Build.VERSION.SdkInt >= BuildVersionCodes.S)
? PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable
: PendingIntentFlags.UpdateCurrent;
var pendingIntent = PendingIntent.GetActivity(Platform.AppContext, pendingIntentId++, intent, pendingIntentFlags);
var builder = new NotificationCompat.Builder(Platform.AppContext, channelId)
.SetContentIntent(pendingIntent)
.SetContentTitle(title)
.SetContentText(message)
.SetSmallIcon(Resource.Drawable.dotnet_logo)
.SetAutoCancel(true);
compatManager.Notify(messageId++, builder.Build());
}
void CreateNotificationChannel()
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
var channel = new NotificationChannel(channelId, new Java.Lang.String(channelName), NotificationImportance.Default)
{
Description = channelDescription
};
var manager = (NotificationManager)Platform.AppContext.GetSystemService(Context.NotificationService);
manager.CreateNotificationChannel(channel);
channelInitialized = true;
}
}
long GetNotifyTime(DateTime notifyTime)
{
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(notifyTime);
double epochDiff = (new DateTime(1970, 1, 1) - DateTime.MinValue).TotalSeconds;
return utcTime.AddSeconds(-epochDiff).Ticks / 10000;
}
}AlarmHandler.cs
using Android.App;
using Android.Content;
namespace YOUR_NAMESPACE.Platforms.Android;
[BroadcastReceiver(Enabled = true, Label = "Local Notifications Broadcast Receiver")]
public class AlarmHandler : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
if (intent?.Extras != null)
{
string title = intent.GetStringExtra(NotificationManagerService.TitleKey);
string message = intent.GetStringExtra(NotificationManagerService.MessageKey);
var manager = NotificationManagerService.Instance ?? new NotificationManagerService();
manager.Show(title, message);
}
}
}NotificationPermission.cs
using Android;
namespace YOUR_NAMESPACE.Platforms.Android;
public class NotificationPermission : Permissions.BasePlatformPermission
{
public override (string androidPermission, bool isRuntime)[] RequiredPermissions
{
get
{
var result = new List<(string androidPermission, bool isRuntime)>();
if (OperatingSystem.IsAndroidVersionAtLeast(33))
result.Add((Manifest.Permission.PostNotifications, true));
return result.ToArray();
}
}
}AndroidManifest.xml
Add inside <manifest>:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />MainActivity.cs Modifications
Set LaunchMode = LaunchMode.SingleTop on the Activity attribute, then add:
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
CreateNotificationFromIntent(Intent);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
CreateNotificationFromIntent(intent);
}
static void CreateNotificationFromIntent(Intent intent)
{
if (intent?.Extras != null)
{
string title = intent.GetStringExtra(NotificationManagerService.TitleKey);
string message = intent.GetStringExtra(NotificationManagerService.MessageKey);
var service = IPlatformApplication.Current.Services.GetService<INotificationManagerService>();
service.ReceiveNotification(title, message);
}
}iOS / Mac Catalyst Implementation
Place in Platforms/iOS/ (and copy/share to Platforms/MacCatalyst/):
NotificationManagerService.cs
using Foundation;
using UserNotifications;
namespace YOUR_NAMESPACE.Platforms.iOS;
public class NotificationManagerService : INotificationManagerService
{
int messageId = 0;
bool hasNotificationsPermission;
public event EventHandler? NotificationReceived;
public NotificationManagerService()
{
UNUserNotificationCenter.Current.Delegate = new NotificationReceiver();
UNUserNotificationCenter.Current.RequestAuthorization(UNAuthorizationOptions.Alert, (approved, err) =>
{
hasNotificationsPermission = approved;
});
}
public void SendNotification(string title, string message, DateTime? notifyTime = null)
{
if (!hasNotificationsPermission) return;
messageId++;
var content = new UNMutableNotificationContent
{
Title = title, Subtitle = "", Body = message, Badge = 1
};
UNNotificationTrigger trigger = notifyTime != null
? UNCalendarNotificationTrigger.CreateTrigger(GetNSDateComponents(notifyTime.Value), false)
: UNTimeIntervalNotificationTrigger.CreateTrigger(0.25, false);
var request = UNNotificationRequest.FromIdentifier(messageId.ToString(), content, trigger);
UNUserNotificationCenter.Current.AddNotificationRequest(request, (err) =>
{
if (err != null) throw new Exception($"Failed to schedule notification: {err}");
});
}
public void ReceiveNotification(string title, string message)
{
NotificationReceived?.Invoke(null, new NotificationEventArgs { Title = title, Message = message });
}
NSDateComponents GetNSDateComponents(DateTime dateTime) => new()
{
Month = dateTime.Month, Day = dateTime.Day, Year = dateTime.Year,
Hour = dateTime.Hour, Minute = dateTime.Minute, Second = dateTime.Second
};
}NotificationReceiver.cs
using UserNotifications;
namespace YOUR_NAMESPACE.Platforms.iOS;
public class NotificationReceiver : UNUserNotificationCenterDelegate
{
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
ProcessNotification(notification);
completionHandler(OperatingSystem.IsIOSVersionAtLeast(14)
? UNNotificationPresentationOptions.Banner
: UNNotificationPresentationOptions.Alert);
}
public override void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler)
{
if (response.IsDefaultAction) ProcessNotification(response.Notification);
completionHandler();
}
void ProcessNotification(UNNotification notification)
{
string title = notification.Request.Content.Title;
string message = notification.Request.Content.Body;
var service = IPlatformApplication.Current?.Services.GetService<INotificationManagerService>();
service?.ReceiveNotification(title, message);
}
}DI Registration in MauiProgram.cs
#if ANDROID
builder.Services.AddTransient<INotificationManagerService,
Platforms.Android.NotificationManagerService>();
#elif IOS
builder.Services.AddTransient<INotificationManagerService,
Platforms.iOS.NotificationManagerService>();
#elif MACCATALYST
builder.Services.AddTransient<INotificationManagerService,
Platforms.MacCatalyst.NotificationManagerService>();
#endifUsage Examples
Request Permission (Android 13+)
#if ANDROID
PermissionStatus status = await Permissions.RequestAsync<Platforms.Android.NotificationPermission>();
#endifSend Notifications
// Immediate
notificationManager.SendNotification("Title", "Message body");
// Scheduled (10 seconds from now)
notificationManager.SendNotification("Reminder", "Time to check in!", DateTime.Now.AddSeconds(10));Receive Notifications
notificationManager.NotificationReceived += (sender, args) =>
{
var data = (NotificationEventArgs)args;
MainThread.BeginInvokeOnMainThread(() =>
{
// Update UI with data.Title and data.Message
});
};