Push notifikacije u .NET MAUI 10: Firebase FCM, APNs i lokalne notifikacije (2026)
Praktični vodič za push notifikacije u .NET MAUI 10: Firebase FCM v1, APNs, dozvole na Androidu 13+, lokalne notifikacije i deep linking s produkcijskim primjerima koda.
Push notifikacije u .NET MAUI implementiraju se kombinacijom Firebase Cloud Messaginga (FCM) za Android, Apple Push Notification servisa (APNs) za iOS i biblioteke Plugin.FirebasePushNotifications koja objedinjuje oba SDK-a u zajednički C# API. Cijela integracija sastoji se od registracije uređaja, dobivanja tokena, obrade payloadova u prednjem i pozadinskom stanju te rukovanja dozvolama za Android 13+ i iOS. U ovom vodiču pokazujem produkcijski workflow koji koristim u timovima od 2024. naovamo, zajedno s lokalnim notifikacijama i deep linkingom.
Za .NET MAUI 9/10 preporučujem Plugin.FirebasePushNotifications jer podržava FCM v1 API koji je Google učinio obveznim za sve nove projekte od lipnja 2024.
APNs zahtijeva .p8 autentikacijski ključ i uploada se u Firebase konzolu; sam Xcode projekt mora imati capability "Push Notifications" i "Background Modes → Remote notifications".
Android 13 (API 33) uveo je runtime dozvolu POST_NOTIFICATIONS koju morate tražiti eksplicitno, inače nema ni jedne prikazane notifikacije.
Data-only payload (samo data ključ) daje potpunu kontrolu nad prikazom u foreground, background i terminated stanju, dok mixed payload OS prikazuje automatski.
Za produkcijsku razinu skalabilnosti razmislite o Azure Notification Hubsu koji apstrahira FCM, APNs i WNS iza jednog registracijskog sučelja.
Lokalne notifikacije (bez remote servera) radite s Plugin.LocalNotification, koji je koristan za podsjetnike, alarme i offline scenarije.
Kako funkcioniraju push notifikacije u .NET MAUI
Kad razgovaram s timovima koji prvi put uvode push u MAUI projekt, prvo objasnim jednu stvar: .NET MAUI sam po sebi nema ugrađenu apstrakciju za remote push. Riječ je o tankom sloju koji delegira komunikaciju prema Google Play Servicesima (za Android), odnosno APNs endpointima (za iOS). Naš je posao kao developera registrirati uređaj, dobiti token, poslati ga na backend i pravilno reagirati kad OS dostavi payload.
Tipičan tok izgleda ovako: aplikacija se pokrene, traži dozvolu, registrira se na FCM/APNs, prima token, šalje token na naš server, server šalje payload prema FCM ili APNs, i OS dostavlja poruku klijentu. Za Android, otkad je Google u lipnju 2024. deprecirao Legacy HTTP endpoint (detalji u službenom FCM v1 migration FAQ-u), jedini podržani format su HTTP v1 pozivi s OAuth 2.0 autentikacijom. Ako pronađete stariji tutorial koji koristi server key, ignorirajte ga; to više ne radi.
iOS strana koristi APNs izravno ili preko FCM-a kao proxya. Većina timova radi preko FCM-a jer to znači jedan payload format za oba OS-a, jedan backend i jedan set analitike. U .NET MAUI 10, koji je izašao u studenom 2025., nema nikakvih bitnih promjena u načinu registracije. Sve i dalje ovisi o platformskim handlerima u Platforms/Android/MainApplication.cs i Platforms/iOS/AppDelegate.cs.
Postavljanje Firebase Cloud Messaginga za Android
Krenimo od Firebase konzole. Idite na console.firebase.google.com, kreirajte projekt i dodajte Android aplikaciju s vašim packageName vrijednosti (mora se poklapati s ApplicationId u .csproj). Preuzmite google-services.json i stavite ga u Platforms/Android/.
U MauiProgram.cs registriramo plugin i prosljeđujemo mu konfiguraciju:
using Plugin.FirebasePushNotifications;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseFirebasePushNotifications(options =>
{
options.QueueFactory = new PersistentQueueFactory();
options.Android.NotificationChannels = new[]
{
new NotificationChannelRequest
{
ChannelId = "default",
ChannelName = "Općenite notifikacije",
Importance = NotificationImportance.High
}
};
});
return builder.Build();
}
U Platforms/Android/MainApplication.cs pozovite inicijalizator prije nego što aplikacija završi start:
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership) { }
public override void OnCreate()
{
base.OnCreate();
Firebase.FirebaseApp.InitializeApp(this);
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
Notifikacijski kanali su obavezni od Androida 8.0 (API 26). Kanal se kreira jednom; nakon toga korisnik može mijenjati postavke (zvuk, vibraciju, važnost) po kanalu, ali sam kod ih više ne može izmijeniti bez deinstalacije. Zato ih dobro dizajnirajte unaprijed. Odvojite marketing, transakcije i chat poruke, i nemojte sve trpati u jedan kanal.
Konfiguracija APNs za iOS u .NET MAUI
Apple strana zahtijeva par koraka u Apple Developer portalu i u Xcode projektu koji MAUI generira. Prvo, u Apple Developer → Keys sekciji generirajte novi APNs Authentication Key (.p8). Zapišite Key ID i Team ID, trebat će vam u Firebase konzoli.
U Firebase konzoli otvorite Project Settings → Cloud Messaging → Apple app configuration → APNs Authentication Key i uploadajte .p8 datoteku zajedno s Key ID-om i Team ID-om. Ovaj pristup je bolji od legacy .p12 jer ključ vrijedi za sve vaše aplikacije i ne istječe godišnje.
U MAUI projektu dodajte Entitlements.plist u Platforms/iOS/:
using Foundation;
using Plugin.FirebasePushNotifications.Platforms;
using UIKit;
[Register(nameof(AppDelegate))]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override void RegisteredForRemoteNotifications(
UIApplication application, NSData deviceToken)
{
Firebase.CloudMessaging.Messaging.SharedInstance.ApnsToken = deviceToken;
}
public override void DidReceiveRemoteNotification(
UIApplication application,
NSDictionary userInfo,
Action<UIBackgroundFetchResult> completionHandler)
{
FirebasePushNotificationManager.DidReceiveMessage(userInfo);
completionHandler(UIBackgroundFetchResult.NewData);
}
}
U Xcode projektnim capabilityjima morate uključiti "Push Notifications" i "Background Modes → Remote notifications". Ako to zaboravite, iOS jednostavno ne dostavlja poruke i nema jasne greške u logu. Iskreno, na ovome sam znala potrošiti pola dana prije nego što sam shvatila da checkbox jednostavno nije uključen.
Kako riješiti dozvole za notifikacije na Androidu 13+ i iOS-u
Ovo je područje gdje smo u našim timovima najviše puta pogriješili. Do Androida 12 su notifikacijske dozvole bile "opt-out": aplikacija ih ima čim je instalirana. Od Androida 13 (API 33) uvedena je runtime dozvola POST_NOTIFICATIONS koju morate tražiti eksplicitno, kao što tražite dozvolu za kameru. Bez toga korisnik ne dobiva niti jednu notifikaciju, čak ni lokalnu.
public async Task<bool> RequestNotificationPermissionAsync()
{
if (DeviceInfo.Platform == DevicePlatform.Android
&& DeviceInfo.Version.Major >= 13)
{
var status = await Permissions.RequestAsync<Permissions.PostNotifications>();
return status == PermissionStatus.Granted;
}
if (DeviceInfo.Platform == DevicePlatform.iOS)
{
var options = UNAuthorizationOptions.Alert
| UNAuthorizationOptions.Badge
| UNAuthorizationOptions.Sound;
var (granted, _) = await UNUserNotificationCenter.Current
.RequestAuthorizationAsync(options);
return granted;
}
return true;
}
Iskustvo iz produkcije: nemojte tražiti dozvolu odmah pri prvom pokretanju. Konverzija je znatno viša ako korisniku prvo pokažete zašto će notifikacije biti korisne, recimo, ekran koji ilustrira "javljamo vam kad stigne odgovor na vašu poruku". Tu istu logiku preciznije razrađujem u vodiču o optimizaciji performansi u .NET MAUI, gdje inicijalno vrijeme starta i UX prvog dojma direktno utječu na dozvole i retenciju.
Lokalne notifikacije bez servera
Ne trebate uvijek remote push. Za podsjetnike, alarme, offline poruke i lokalne događaje (npr. korisnik je stigao na lokaciju) koristimo Plugin.LocalNotification. On je manji, ne zahtijeva Firebase i radi u potpunosti klijentski.
builder.UseLocalNotification(config =>
{
config.AddCategory(new NotificationCategory(NotificationCategoryType.Status)
{
ActionList = new HashSet<NotificationAction>
{
new(101) { Title = "Otvori", Android = new AndroidOptions { LaunchAppWhenTapped = true } }
}
});
});
Slanje lokalne notifikacije s odgodom od 30 sekundi:
public async Task ScheduleReminderAsync(string title, string body)
{
var request = new NotificationRequest
{
NotificationId = Random.Shared.Next(1, 100_000),
Title = title,
Description = body,
CategoryType = NotificationCategoryType.Reminder,
Schedule = new NotificationRequestSchedule
{
NotifyTime = DateTime.Now.AddSeconds(30),
NotifyRepeatInterval = TimeSpan.FromMinutes(30)
}
};
await LocalNotificationCenter.Current.Show(request);
}
Zapamtite jednu stvar. Raspoređene lokalne notifikacije brišu se kad korisnik "force-quit"-a aplikaciju na iOS-u (za razliku od Androida). Ako aplikacija ovisi o dugoročnim podsjetnicima, kombinirajte lokalne s server-side push notifikacijama kao fallback.
Obrada foreground, background i tapnutih notifikacija
Najviše bugova u produkciji vidjela sam upravo oko različitog ponašanja u tri stanja aplikacije: foreground (otvorena, u prvom planu), background (otvorena, ali skrivena) i terminated (ubijena). Postoji razlika između notification i data payloadova, i preporučujem da uvijek radite s data-only payloadovima.
Notification vs data payload
Notification payload:
{
"message": {
"token": "<fcm_token>",
"notification": {
"title": "Nova poruka",
"body": "Ivan vam je poslao poruku"
}
}
}
Ovakav payload OS prikazuje automatski u backgroundu i nikada ne zove vaš kod. To znači bez custom prikaza, bez badge ažuriranja, bez analitike.
Ovo dostavlja poruku vašem kodu u svim stanjima, a vi kontrolirate prikaz. Registracija handlera:
public partial class App : Application
{
public App()
{
InitializeComponent();
CrossFirebasePushNotification.Current.NotificationReceived += (s, p) =>
{
// App je u foregroundu, ručno prikazujemo notifikaciju ili in-app toast
LocalNotificationCenter.Current.Show(new NotificationRequest
{
NotificationId = Random.Shared.Next(1, 100_000),
Title = p.Data["title"]?.ToString(),
Description = p.Data["body"]?.ToString()
});
};
CrossFirebasePushNotification.Current.NotificationOpened += (s, p) =>
{
// Korisnik je tapnuo notifikaciju, prebacujemo ga u konverzaciju
var conversationId = p.Data["conversationId"]?.ToString();
Shell.Current.GoToAsync($"//chat?id={conversationId}");
};
}
}
Deep linking iz push notifikacija
Push bez dobrog deep linkinga je promašena prilika. Kad korisnik tapne notifikaciju "Ivan vam je poslao poruku", aplikacija ga mora odvesti direktno u tu konverzaciju, ne na naslovnicu s koje mora sam kliktati dalje. .NET MAUI Shell podržava ovo elegantno kroz URI rutiranje.
CrossFirebasePushNotification.Current.NotificationOpened += async (s, p) =>
{
if (p.Data.TryGetValue("route", out var routeObj)
&& routeObj is string route)
{
await MainThread.InvokeOnMainThreadAsync(() =>
Shell.Current.GoToAsync(route));
}
};
Pripazite na "cold start" scenarij. Kad je aplikacija terminated i korisnik tapne notifikaciju, event NotificationOpened zna okinuti prije nego što je Shell inicijaliziran. Točno na tom bagu sam izgubila cijelu subotu prošle godine. Rješenje je čekanje na Shell.Current u loopu ili spremanje pending route u Preferences i navigacija nakon što je Shell spreman.
Azure Notification Hubs kao jedinstveni backend
Ako imate veći broj korisnika (recimo, preko 100.000 aktivnih uređaja), direktna komunikacija s FCM-om i APNs-om iz vlastitog servera postaje operativno teška. Fanout notifikacija milijunima korisnika u sekundama, retry logika, upravljanje neaktivnim tokenima, segmentacija: sve to trebate izgraditi sami. Microsoftov službeni vodič za push notifikacije u .NET MAUI 10 pokazuje pattern gdje Azure Notification Hubs stoji između vašeg backend servisa i platforme.
Prednosti su značajne: jedinstven registracijski model (jedan poziv za registraciju uređaja bez obzira na platformu), tag-based segmentacija (šaljite grupama korisnika kroz tag "vip" ili "region-hr"), rich analytics i SLA-driven dostava. Mana je cijena. Za manje projekte je vjerojatno prevelika.
Backend registracija tokena preko Azure Notification Hubsa:
public async Task RegisterDeviceAsync(string fcmToken, string userId)
{
using var http = new HttpClient();
var payload = new
{
installationId = DeviceInfo.Current.Idiom + "-" + userId,
platform = DeviceInfo.Platform == DevicePlatform.Android ? "fcmv1" : "apns",
pushChannel = fcmToken,
tags = new[] { $"user:{userId}", "region:hr" }
};
await http.PutAsJsonAsync(
"https://api.mojbackend.hr/notifications/register",
payload);
}
U našim timovima uvijek postoji staging FCM projekt odvojen od produkcije. Miješati testne i prave uređaje u istoj Firebase konzoli je ozbiljan rizik. Jedan pogrešan klik u konzoli, i svi produkcijski korisnici dobiju "Test message: Hello world". Postavite dva projekta i preko build konfiguracije zamijenite google-services.json ili GoogleService-Info.plist.
Brzo testiranje iz Firebase konzole
Najbrži smoke test: u Firebase konzoli otvorite Cloud Messaging, pa New campaign, pa Notification. Unesite naslov, tekst, ciljajte specifičan FCM token, i pošaljite. Ako notifikacija stigne, cijeli lanac (SDK, token, dozvole, kanali) radi.
Testiranje CLI alatima
Za data-only payload koristite curl ili Postman. Google zahtijeva OAuth 2.0, pa prvo generirajte access token iz service account JSON datoteke:
Bundle ID mismatch: ako Xcode bundle identifier ne odgovara onome u Firebaseu, iOS neće registrirati token. Provjerite oba i uklonite razmake.
Notifikacije rade u debug modu, ali ne u release: APNs environment je development vs production. Za TestFlight i App Store, mora biti production.
Battery optimizations: na nekim Android uređajima (Xiaomi, Huawei) OS agresivno ubija background procese, i notifikacije kasne satima. Riješite s uputom korisniku kako isključiti battery optimization za vašu aplikaciju.
Zaboravljeni content-available na iOS-u: bez ove zastavice, iOS ne budi aplikaciju za data-only payload.
Token rotacija: FCM tokeni se povremeno mijenjaju. Registrirajte se na OnTokenRefresh i sinkronizirajte novi token s backendom.
Kad implementaciju uklopite u CI pipeline, provjerite da secrets za APNs (.p8) i FCM (service account JSON) ne završe u repozitoriju. Detaljnije o secret managementu u pipelineu je u našem vodiču o CI/CD za .NET MAUI aplikacije s GitHub Actionsom i Azure DevOpsom.
Često postavljana pitanja
Podržava li .NET MAUI push notifikacije bez third-party biblioteka?
Djelomično. Za lokalne notifikacije možete napisati vlastiti servis koristeći platformske API-je (NotificationCompat na Androidu, UNUserNotificationCenter na iOS-u). Za remote FCM/APNs push, u praksi su neizbježne biblioteke poput Plugin.FirebasePushNotifications jer bi vlastita implementacija Firebase Messaging SDK-a i APNs handshakea bila stotine linija boilerplate koda po platformi.
Koja je razlika između FCM-a i APNs-a i moram li koristiti oba?
APNs je Appleov servis, obvezan za sve notifikacije na iOS-u. FCM je Googleov servis koji standardno prima poruke za Android, ali može funkcionirati i kao proxy za APNs (vi šaljete jednu FCM poruku, a FCM je preusmjerava na APNs za iOS korisnike). Time šaljete oba OS-a iz jednog backend endpointa i s jednim SDK-om na klijentu.
Zašto notifikacije ne stižu na Android emulatoru?
Najčešće jer emulator koristi image bez Google Play Services. Kreirajte novi AVD u Android Studiju s "Google Play" oznakom (ne "Google APIs"), i registrirajte tester Google račun kroz Play Store. Alternativno, koristite fizički uređaj; pouzdaniji je za testiranje FCM-a.
Kako obraditi push notifikaciju kad je aplikacija zatvorena (terminated)?
Koristite data-only payload s priority: high na Androidu i content-available: 1 na iOS-u. OS tada budi aplikaciju kratko da procesira poruku. Vaš handler mora biti registriran vrlo rano u lifecycle-u (u MainApplication.OnCreate na Androidu i AppDelegate.FinishedLaunching na iOS-u), inače OS pošalje poruku prije nego što je slušatelj spreman.
Trebam li Azure Notification Hubs ili je dovoljno slati direktno iz backenda na FCM?
Za manje projekte (do ~50.000 korisnika) direktan pristup FCM-u je jednostavniji i jeftiniji. Za veće baze korisnika, kompleksne segmentacije, ili ako podržavate i Windows/WNS uređaje, Azure Notification Hubs isplati se zbog jedinstvenog registracijskog sučelja, tag-based targetinga i skalabilnog fanouta.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.