Push Notifications in .NET MAUI: Local Alerts, Firebase FCM, and APNs for Android and iOS

Learn how to set up local notifications, Firebase Cloud Messaging, and APNs in .NET MAUI for Android and iOS. Covers permission handling, deep linking, topic subscriptions, and production-ready patterns for .NET 10.

Why Notifications Matter More Than You Think

Push notifications are, without exaggeration, the heartbeat of modern mobile apps. They're what bring users back, deliver time-sensitive info, and create the engagement loops that separate abandoned installs from daily active users. And yet — implementing notifications in .NET MAUI remains one of the most fragmented and poorly documented areas of cross-platform development.

The challenge is real.

Android and iOS handle notifications completely differently under the hood. Android requires notification channels, runtime permissions starting from API 33, and careful handling of foreground vs background message delivery. iOS demands provisioning profiles, entitlements, and a relationship with Apple Push Notification service (APNs) that can feel bureaucratic at best. And if you want remote push notifications? You'll need a server-side component — whether that's Firebase Cloud Messaging, Azure Notification Hubs, or something custom.

This guide walks you through all three notification strategies in .NET MAUI targeting .NET 10: local notifications that fire on-device, Firebase Cloud Messaging for remote pushes, and the platform-specific configuration needed to make everything work on both Android and iOS. By the end, you'll have production-ready code patterns and the knowledge to pick the right approach for your app.

Understanding the Three Notification Models

Before writing any code, it helps to understand what notification infrastructure you actually need. The choice really comes down to who triggers the notification and where the logic lives.

Local Notifications

Local notifications are scheduled and triggered entirely on the device. No server, no cloud service, no network connection required. They're ideal for reminders, alarms, recurring prompts, and timer-based alerts. The app itself decides when to fire the notification using the operating system's scheduling APIs.

Remote Push Notifications (Firebase / APNs)

Remote push notifications originate from a server and get delivered through platform-specific gateways — Firebase Cloud Messaging for Android and Apple Push Notification service for iOS. They require network connectivity and a backend service that sends the notification payload. Use them for real-time messaging, order updates, breaking news, or any scenario where the trigger lives outside the device.

Hybrid Approach

Most production apps end up using both. A chat app sends remote pushes for incoming messages but uses local notifications for draft reminders. An e-commerce app pushes order status updates remotely but schedules local deal alerts. Understanding these trade-offs helps you architect things cleanly from the start.

FeatureLocal NotificationsRemote Push (FCM/APNs)
Network requiredNoYes
Server requiredNoYes
SchedulingOn-deviceServer-driven
Delivery guaranteeHigh (device-local)Best-effort
Use casesReminders, timers, alarmsMessages, updates, alerts
Platform setup complexityLowHigh

Implementing Local Notifications in .NET MAUI

.NET MAUI gives you a cross-platform abstraction for local notifications through the INotificationManagerService interface. This approach lets you define a single API surface that each platform implements natively. Let's set it up from scratch.

Step 1: Define the Cross-Platform Interface

Create a shared interface that all platform implementations will satisfy. This goes in your shared project folder.

public interface INotificationManagerService
{
    event EventHandler NotificationReceived;
    void SendNotification(string title, string message, DateTime? notifyTime = null);
    void ReceiveNotification(string title, string message);
}

public class NotificationEventArgs : EventArgs
{
    public string Title { get; set; } = string.Empty;
    public string Message { get; set; } = string.Empty;
}

The SendNotification method handles both immediate and scheduled delivery. Pass a DateTime value for scheduled notifications or null for immediate display. The NotificationReceived event lets your UI react when the user taps a notification.

Step 2: Android Implementation

Android requires a notification channel (mandatory since Android 8.0) and the POST_NOTIFICATIONS runtime permission for Android 13 and above. Create this implementation under Platforms/Android.

using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Core.App;

namespace YourApp.Platforms.Android;

public class NotificationManagerService : INotificationManagerService
{
    public const string TitleKey = "title";
    public const string MessageKey = "message";

    private const string ChannelId = "default_channel";
    private const string ChannelName = "Default";
    private const string ChannelDescription = "App notifications";

    private readonly Context _context;
    private readonly NotificationManager _manager;
    private int _notificationId;
    private bool _channelInitialized;

    public event EventHandler? NotificationReceived;

    public NotificationManagerService()
    {
        _context = Platform.AppContext;
        _manager = (NotificationManager)_context
            .GetSystemService(Context.NotificationService)!;
    }

    public void SendNotification(string title, string message,
        DateTime? notifyTime = null)
    {
        EnsureChannelExists();

        if (notifyTime is not null)
        {
            var intent = new Intent(_context, typeof(AlarmReceiver));
            intent.PutExtra(TitleKey, title);
            intent.PutExtra(MessageKey, message);

            var pendingIntentFlags = PendingIntentFlags.UpdateCurrent
                | PendingIntentFlags.Immutable;
            var pendingIntent = PendingIntent.GetBroadcast(
                _context, _notificationId++, intent, pendingIntentFlags);

            var alarmManager = _context
                .GetSystemService(Context.AlarmService) as AlarmManager;
            var triggerTime = GetAlarmTriggerTime(notifyTime.Value);
            alarmManager?.Set(AlarmType.ElapsedRealtime, triggerTime,
                pendingIntent);
        }
        else
        {
            ShowNotification(title, message);
        }
    }

    public void ReceiveNotification(string title, string message)
    {
        NotificationReceived?.Invoke(this, new NotificationEventArgs
        {
            Title = title,
            Message = message
        });
    }

    private void ShowNotification(string title, string message)
    {
        EnsureChannelExists();

        var intent = new Intent(_context, typeof(MainActivity));
        intent.PutExtra(TitleKey, title);
        intent.PutExtra(MessageKey, message);
        intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);

        var pendingIntent = PendingIntent.GetActivity(
            _context, _notificationId, intent,
            PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);

        var builder = new NotificationCompat.Builder(_context, ChannelId)
            .SetContentTitle(title)
            .SetContentText(message)
            .SetSmallIcon(Resource.Drawable.notification_icon)
            .SetContentIntent(pendingIntent)
            .SetAutoCancel(true)
            .SetPriority(NotificationCompat.PriorityDefault);

        _manager.Notify(_notificationId++, builder.Build());
    }

    private void EnsureChannelExists()
    {
        if (_channelInitialized) return;

        if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
        {
            var channel = new NotificationChannel(
                ChannelId, ChannelName, NotificationImportance.Default)
            {
                Description = ChannelDescription
            };
            _manager.CreateNotificationChannel(channel);
        }

        _channelInitialized = true;
    }

    private long GetAlarmTriggerTime(DateTime notifyTime)
    {
        var diff = (notifyTime - DateTime.Now).TotalMilliseconds;
        return SystemClock.ElapsedRealtime() + (long)diff;
    }
}

Step 3: Request the POST_NOTIFICATIONS Permission

Starting with Android 13 (API 33), apps must request the POST_NOTIFICATIONS permission at runtime. In .NET 9 and .NET 10, MAUI includes a built-in Permissions.PostNotifications class so you no longer need a custom permission class. Request it after your first page appears — never in MauiProgram.cs or App.cs. I've seen too many devs try to request it during startup and wonder why the dialog never shows up.

// In your MainPage.xaml.cs or a startup ViewModel
private async Task RequestNotificationPermissionAsync()
{
    if (DeviceInfo.Platform == DevicePlatform.Android)
    {
        var status = await Permissions
            .CheckStatusAsync<Permissions.PostNotifications>();

        if (status != PermissionStatus.Granted)
        {
            status = await Permissions
                .RequestAsync<Permissions.PostNotifications>();
        }

        if (status != PermissionStatus.Granted)
        {
            // Inform the user that notifications are disabled
            await Shell.Current.DisplayAlert(
                "Notifications Disabled",
                "Enable notifications in Settings to receive alerts.",
                "OK");
        }
    }
}

Also add the permission to your AndroidManifest.xml:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Step 4: Configure MainActivity for Notification Taps

When users tap a notification, Android creates a new intent. Your MainActivity needs to intercept this and route the data back to your notification service. This part is easy to overlook, but skip it and your tap handling simply won't work.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    LaunchMode = LaunchMode.SingleTop,
    ConfigurationChanges = ConfigChanges.ScreenSize
        | ConfigChanges.Orientation | ConfigChanges.UiMode)]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        HandleNotificationIntent(Intent);
    }

    protected override void OnNewIntent(Intent? intent)
    {
        base.OnNewIntent(intent);
        HandleNotificationIntent(intent);
    }

    private static void HandleNotificationIntent(Intent? intent)
    {
        if (intent?.Extras == null) return;

        string? title = intent.GetStringExtra(
            NotificationManagerService.TitleKey);
        string? message = intent.GetStringExtra(
            NotificationManagerService.MessageKey);

        if (title is null || message is null) return;

        var service = IPlatformApplication.Current?.Services
            .GetService<INotificationManagerService>();
        service?.ReceiveNotification(title, message);
    }
}

Step 5: iOS Implementation

iOS uses UNUserNotificationCenter for both immediate and scheduled local notifications. Create this implementation under Platforms/iOS.

using Foundation;
using UserNotifications;

namespace YourApp.Platforms.iOS;

public class NotificationManagerService : INotificationManagerService
{
    private int _notificationId;

    public event EventHandler? NotificationReceived;

    public NotificationManagerService()
    {
        UNUserNotificationCenter.Current.RequestAuthorization(
            UNAuthorizationOptions.Alert
            | UNAuthorizationOptions.Badge
            | UNAuthorizationOptions.Sound,
            (approved, error) => { /* Log approval status */ });
    }

    public void SendNotification(string title, string message,
        DateTime? notifyTime = null)
    {
        var content = new UNMutableNotificationContent
        {
            Title = title,
            Body = message,
            Sound = UNNotificationSound.Default
        };

        UNNotificationTrigger trigger;

        if (notifyTime is not null)
        {
            var interval = (notifyTime.Value - DateTime.Now).TotalSeconds;
            if (interval <= 0) interval = 1;
            trigger = UNTimeIntervalNotificationTrigger
                .CreateTrigger(interval, false);
        }
        else
        {
            trigger = UNTimeIntervalNotificationTrigger
                .CreateTrigger(1, false);
        }

        var request = UNNotificationRequest.FromIdentifier(
            _notificationId++.ToString(), content, trigger);

        UNUserNotificationCenter.Current.AddNotificationRequest(
            request, (error) =>
            {
                if (error is not null)
                {
                    System.Diagnostics.Debug
                        .WriteLine($"Notification error: {error.Description}");
                }
            });
    }

    public void ReceiveNotification(string title, string message)
    {
        NotificationReceived?.Invoke(this, new NotificationEventArgs
        {
            Title = title,
            Message = message
        });
    }
}

Step 6: Register Services in MauiProgram.cs

Wire up the platform-specific implementations using conditional compilation. This keeps your dependency injection container clean and makes sure the right implementation loads on each platform.

var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .ConfigureFonts(fonts =>
    {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
    });

#if ANDROID
builder.Services.AddSingleton<INotificationManagerService,
    Platforms.Android.NotificationManagerService>();
#elif IOS
builder.Services.AddSingleton<INotificationManagerService,
    Platforms.iOS.NotificationManagerService>();
#endif

return builder.Build();

Setting Up Firebase Cloud Messaging for Remote Push Notifications

Remote push notifications need a server-side component that sends messages through Google's and Apple's delivery infrastructure. Firebase Cloud Messaging (FCM) is far and away the most popular option because it handles both platforms from a single API. Here's a complete setup walkthrough for .NET MAUI with .NET 10.

Step 1: Create a Firebase Project

Go to the Firebase Console, create a new project, and add both an Android and an iOS app. For Android, enter the package name that matches your ApplicationId in the .csproj file. For iOS, enter the Bundle ID. Download the resulting configuration files: google-services.json for Android and GoogleService-Info.plist for iOS.

Step 2: Install the Plugin

The Plugin.FirebasePushNotifications NuGet package is the most mature cross-platform FCM library for .NET MAUI. It works with .NET 7 and higher, including .NET 10.

dotnet add package Plugin.FirebasePushNotifications

Step 3: Add Firebase Configuration Files to Your Project

Place the downloaded files in the correct locations and update your .csproj to include them with proper build actions:

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-android'">
  <GoogleServicesJson Include="Platforms\Android\google-services.json" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-ios'">
  <BundleResource Include="Platforms\iOS\GoogleService-Info.plist"
                  Link="GoogleService-Info.plist" />
</ItemGroup>

Step 4: Configure MauiProgram.cs

Register the Firebase push notification plugin with optional Android notification channel configuration:

var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .UseFirebasePushNotifications(options =>
    {
#if ANDROID
        options.Android.NotificationChannels = new[]
        {
            new NotificationChannelRequest
            {
                ChannelId = "general",
                ChannelName = "General",
                Description = "General notifications",
                Importance = NotificationImportance.High
            },
            new NotificationChannelRequest
            {
                ChannelId = "promotions",
                ChannelName = "Promotions",
                Description = "Promotional offers",
                Importance = NotificationImportance.Default
            }
        };
#endif
    });

return builder.Build();

Step 5: Set Up MainActivity for Android

Your MainActivity must use LaunchMode.SingleTask to avoid duplicate activity instances when the user taps a notification. Honestly, this is one of those things that's easy to miss but causes really confusing behavior if you get it wrong.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    LaunchMode = LaunchMode.SingleTask,
    ConfigurationChanges = ConfigChanges.ScreenSize
        | ConfigChanges.Orientation | ConfigChanges.UiMode)]
public class MainActivity : MauiAppCompatActivity
{
}

Step 6: Handle Registration and Incoming Messages

Inject IFirebasePushNotification into your ViewModel or page to subscribe to events and manage notification tokens.

public partial class MainViewModel : ObservableObject
{
    private readonly IFirebasePushNotification _pushNotification;

    public MainViewModel(IFirebasePushNotification pushNotification)
    {
        _pushNotification = pushNotification;

        // Subscribe to token refresh events
        _pushNotification.TokenRefreshed += OnTokenRefreshed;

        // Handle notification received while app is in foreground
        _pushNotification.NotificationReceived += OnNotificationReceived;

        // Handle notification tap
        _pushNotification.NotificationOpened += OnNotificationOpened;
    }

    private void OnTokenRefreshed(object? sender,
        FirebasePushNotificationTokenEventArgs e)
    {
        string token = e.Token;
        // Send token to your backend for device registration
        Debug.WriteLine($"FCM Token: {token}");
    }

    private void OnNotificationReceived(object? sender,
        FirebasePushNotificationDataEventArgs e)
    {
        // Notification received in foreground
        var title = e.Data["title"]?.ToString();
        var body = e.Data["body"]?.ToString();
        Debug.WriteLine($"Received: {title} - {body}");
    }

    private void OnNotificationOpened(object? sender,
        FirebasePushNotificationResponseEventArgs e)
    {
        // User tapped the notification - navigate accordingly
        if (e.Data.TryGetValue("orderId", out var orderId))
        {
            MainThread.BeginInvokeOnMainThread(async () =>
            {
                await Shell.Current.GoToAsync(
                    $"orderDetails?id={orderId}");
            });
        }
    }
}

Configuring iOS for APNs Push Notifications

iOS push notifications need more setup than Android. Apple's ecosystem demands code signing, entitlements, and provisioning profiles before your app can receive even a single push. Skip any step and you'll get silent failures that are genuinely painful to debug.

Enable Push Notifications in the Apple Developer Portal

Log into the Apple Developer Portal and navigate to your App ID. Enable the Push Notifications capability. Under the Keys section, create an APNs authentication key — this is a .p8 file you'll upload to Firebase. Note the Key ID and your Team ID, as both are needed for Firebase configuration.

Create the Entitlements.plist

Add a file named Entitlements.plist to the Platforms/iOS folder in your project:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>aps-environment</key>
  <string>development</string>
</dict>
</plist>

Use development for debug builds and production for App Store releases. Mismatching these with your signing certificate causes iOS to silently reject the app during installation — no error, no warning, just nothing works.

Update Info.plist for Background Modes

Enable remote notification background mode so your app can receive silent pushes and process data in the background:

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

Configure AppDelegate for Push Registration

Register for remote notifications in your AppDelegate or via lifecycle events in MauiProgram.cs:

builder.ConfigureLifecycleEvents(lifecycle =>
{
#if IOS
    lifecycle.AddiOS(ios =>
    {
        ios.FinishedLaunching((app, data) =>
        {
            UIKit.UIApplication.SharedApplication
                .RegisterForRemoteNotifications();
            return true;
        });
    });
#endif
});

Upload APNs Key to Firebase

In the Firebase Console, go to Project Settings, then the Cloud Messaging tab. Under Apple app configuration, upload the .p8 APNs authentication key and provide the Key ID and Team ID. This allows Firebase to deliver push notifications through Apple's servers on your behalf.

Testing on the iOS Simulator

The iOS simulator supports remote notifications starting from iOS 16 when running on macOS 13 or later with Apple silicon or T2 processors. Each simulator generates unique registration tokens. If your Mac doesn't meet these requirements, you'll need a physical device with a provisioning profile that includes the Push Notifications capability.

Handling Notification Taps and Deep Linking

Sending notifications is only half the battle. You also need to handle what happens when users actually tap them. A well-implemented notification tap should navigate the user directly to the relevant content — an order detail page, a chat conversation, or a specific settings screen.

Using Custom Data Payloads

Include actionable data in your notification payload so the app knows where to navigate:

// Server-side payload (JSON sent to FCM)
{
  "message": {
    "token": "device_token_here",
    "notification": {
      "title": "Order Shipped",
      "body": "Your order #12345 is on its way!"
    },
    "data": {
      "action": "order_details",
      "orderId": "12345"
    }
  }
}

Routing on Notification Tap

Process the data payload and navigate using Shell routing:

private void OnNotificationOpened(object? sender,
    FirebasePushNotificationResponseEventArgs e)
{
    MainThread.BeginInvokeOnMainThread(async () =>
    {
        var action = e.Data.GetValueOrDefault("action")?.ToString();

        switch (action)
        {
            case "order_details":
                var orderId = e.Data["orderId"]?.ToString();
                await Shell.Current.GoToAsync(
                    $"//orders/details?id={orderId}");
                break;

            case "chat":
                var chatId = e.Data["chatId"]?.ToString();
                await Shell.Current.GoToAsync(
                    $"//chats/conversation?id={chatId}");
                break;

            default:
                await Shell.Current.GoToAsync("//home");
                break;
        }
    });
}

Always wrap navigation in MainThread.BeginInvokeOnMainThread because notification callbacks can arrive on background threads. Attempting to manipulate Shell navigation off the main thread causes crashes that are really hard to reproduce consistently.

Topic Subscriptions and Targeted Messaging

Firebase Cloud Messaging supports topic-based subscriptions, which let you send notifications to groups of users without managing individual device tokens server-side. This is great for category-based alerts, regional notifications, or feature-specific updates.

// Subscribe to topics
await _pushNotification.SubscribeAsync("breaking_news");
await _pushNotification.SubscribeAsync("deals_electronics");

// Unsubscribe when user opts out
await _pushNotification.UnsubscribeAsync("deals_electronics");

// Get all current subscriptions
var topics = _pushNotification.SubscribedTopics;

Topic subscriptions persist across app restarts. The subscription state is managed by the FCM SDK itself, so you don't need to store it locally. That said, it's good practice to mirror subscription preferences in your app's settings UI so users can manage them explicitly.

Best Practices for Production Notifications

Respect User Preferences

Provide granular notification settings within your app. Let users control which types of notifications they receive — marketing, transactional, social, and so on. Store these preferences both locally and on your server. When a user disables a category, stop sending that type from the backend rather than silently dropping it on the device.

Handle Token Refresh Correctly

FCM tokens aren't permanent. They can change when the app is restored on a new device, when the user clears app data, or when the FCM SDK determines a refresh is needed. Always listen for token refresh events and update your backend accordingly. Stale tokens lead to failed deliveries and skewed analytics — and those failures are completely silent, which makes them tricky to catch.

Use Silent Notifications for Background Data Sync

Silent (data-only) notifications wake your app in the background without showing anything to the user. They're useful for triggering data synchronization, updating cached content, or refreshing badges. On Android, send a data-only message (no notification key). On iOS, set content-available: 1 in the APNs payload.

Test Across All App States

Notification behavior differs significantly depending on the app state. This is something that trips up a lot of developers:

  • Foreground: On Android, FCM doesn't automatically display the notification — your app must handle it explicitly. On iOS, you need a UNUserNotificationCenterDelegate to show it.
  • Background: The OS displays the notification automatically. Your code only runs when the user taps it.
  • Terminated: Same as background for display, but your app's launch path must handle the notification intent or user activity correctly.

Test all three states on both platforms before shipping. The most common bugs in notification handling come from untested state transitions, and they're the kind of issues that only show up in production.

Notification Channel Strategy for Android

Define notification channels deliberately at app startup. Once a channel is created, its importance level can't be changed programmatically — only the user can modify it through system settings. If you need to change a channel's behavior, you have to create a new channel with a different ID and migrate users to it. So plan your channel hierarchy early:

// Good channel design
new NotificationChannelRequest { ChannelId = "orders",
    ChannelName = "Order Updates",
    Importance = NotificationImportance.High },

new NotificationChannelRequest { ChannelId = "marketing",
    ChannelName = "Deals & Promotions",
    Importance = NotificationImportance.Low },

new NotificationChannelRequest { ChannelId = "social",
    ChannelName = "Messages & Comments",
    Importance = NotificationImportance.Default }

Troubleshooting Common Notification Issues

Notifications Not Appearing on Android 13+

If notifications aren't showing, the most likely cause is the missing POST_NOTIFICATIONS permission. Check that you've declared it in AndroidManifest.xml, requested it at runtime after the first page appears, and that the user has actually granted it. You can verify the current permission status with Permissions.CheckStatusAsync<Permissions.PostNotifications>().

iOS Push Notifications Work in Debug But Fail in Release

This usually means you have an entitlements mismatch. Verify that Entitlements.plist uses development for debug builds and production for release builds. Also check that your provisioning profile includes the Push Notifications capability and matches the signing certificate type (development vs distribution). I've personally spent hours chasing this one — it's almost always the entitlements.

FCM Token Is Null on iOS Simulator

Remote notification registration on the iOS simulator requires iOS 16 or later running on macOS 13+ with Apple silicon or a T2 chip. On older hardware, use a physical device for push notification testing.

Notifications Arrive But the App Does Not Navigate

If tapping a notification opens the app but doesn't navigate to the expected page, check three things. First, make sure your MainActivity uses LaunchMode.SingleTop or LaunchMode.SingleTask. Second, confirm that OnNewIntent handles the notification data. Third, ensure the navigation code runs on the main thread with MainThread.BeginInvokeOnMainThread.

Duplicate Notifications on Android

When a notification includes both a notification and data payload and the app is in the foreground, Firebase triggers the OnMessageReceived callback. If your code also creates a notification manually, you'll see duplicates. The fix is to use data-only messages when you need full control over notification presentation, or skip manual creation when the app is foregrounded.

Plugin.LocalNotification: A Quick Alternative

If you need local notifications without writing platform-specific code from scratch, the Plugin.LocalNotification NuGet package provides a ready-made cross-platform abstraction for Android and iOS.

dotnet add package Plugin.LocalNotification

Scheduling a notification is a single method call:

using Plugin.LocalNotification;

var request = new NotificationRequest
{
    NotificationId = 1,
    Title = "Reminder",
    Description = "Don't forget to check your daily goals!",
    Schedule = new NotificationRequestSchedule
    {
        NotifyTime = DateTime.Now.AddMinutes(30)
    },
    Android = new AndroidOptions
    {
        ChannelId = "reminders",
        Priority = AndroidNotificationPriority.High
    }
};

await LocalNotificationCenter.Current.Show(request);

The plugin also supports recurring notifications with ShowDaily, ShowWeekly, and ShowHourly methods. It handles notification channel creation automatically on Android and permission requests on iOS. The trade-off is less customization compared to implementing INotificationManagerService yourself, but for most use cases it's more than sufficient.

Choosing the Right Notification Architecture

Your notification strategy should match your app's complexity and requirements. Here's a practical decision framework:

  • Simple reminders or timer apps: Use Plugin.LocalNotification for minimal code and fast setup.
  • Apps that need server-triggered alerts: Add Firebase Cloud Messaging with Plugin.FirebasePushNotifications for a straightforward remote push setup.
  • Enterprise apps with multiple platforms and high scale: Consider Azure Notification Hubs as a backend intermediary between your server and FCM/APNs. It adds cost but simplifies token management and provides analytics.
  • Apps requiring both local and remote: Combine Plugin.FirebasePushNotifications for remote pushes with the built-in INotificationManagerService pattern (or Plugin.LocalNotification) for on-device scheduling.

Whatever architecture you choose, test early and test across both platforms. Notification bugs discovered after release are among the hardest to debug because they depend on app state, OS version, and user permission choices that you simply can't control.

Frequently Asked Questions

Does .NET MAUI have built-in push notification support?

.NET MAUI includes built-in support for local notifications through the INotificationManagerService interface pattern and platform-specific implementations. However, remote push notifications (FCM/APNs) require third-party packages like Plugin.FirebasePushNotifications or integration with Azure Notification Hubs. There's no built-in, out-of-the-box push notification API in the framework itself.

How do I test push notifications on the iOS simulator?

The iOS simulator supports remote notifications starting from iOS 16, but only when running on macOS 13 or later with Apple silicon (M1/M2/M3/M4) or T2 processors. Each simulator generates unique FCM tokens. If your hardware doesn't meet these requirements, you'll need a physical iOS device with a valid provisioning profile that includes Push Notifications capability.

Why are my notifications not showing on Android 13?

Android 13 (API 33) introduced the POST_NOTIFICATIONS runtime permission. Apps targeting Android 12 or lower will see an automatic permission prompt when the first notification channel is created. Apps targeting Android 13+ must explicitly request this permission using Permissions.RequestAsync<Permissions.PostNotifications>(). If the user denies the permission, no notifications will appear. Check and guide users to system settings if permission is denied.

Can I send push notifications to both Android and iOS from a single backend?

Yes. Firebase Cloud Messaging (FCM) supports both Android and iOS. You send a single API request to the FCM HTTP v1 endpoint with a device token, and Firebase routes the message through the appropriate delivery channel — FCM for Android and APNs for iOS. Alternatively, Azure Notification Hubs provides a unified API that abstracts FCM, APNs, and WNS (Windows) behind a single registration and send interface.

What is the difference between a notification payload and a data payload in FCM?

A notification payload (the "notification" key) is handled automatically by the OS when the app is in the background — the system displays the notification without calling your app code. A data payload (the "data" key) is always delivered to your app's message handler, giving you full control over how and whether to display a notification. For maximum flexibility, use data-only payloads so your app controls the presentation in all states — foreground, background, and terminated.

About the Author Editorial Team

Our team of expert writers and editors.