Notificações Push no .NET MAUI: Guia Completo com Firebase e APNs em 2026

Implemente notificações push no .NET MAUI com Firebase Cloud Messaging e APNs. Guia prático em 2026 com Plugin.Firebase, deep linking, silent push e Azure Notification Hubs.

Push Notifications no .NET MAUI 2026

Atualizado: 26 de junho de 2026

Implementar notificações push no .NET MAUI exige integrar Firebase Cloud Messaging (FCM) no Android e Apple Push Notification service (APNs) no iOS, geralmente através do pacote Plugin.Firebase ou Shiny.Push. Em produção, recomendo encapsular ambas as plataformas atrás de uma única interface, registar o token do dispositivo num backend e tratar payloads de dados em background. Caso contrário, o iOS silenciosamente descarta mensagens quando o app está fechado. Este guia mostra o caminho completo em 2026, com código que rodei em três apps na App Store e Google Play.

  • FCM HTTP v1 substituiu a API legacy em junho de 2024. Quem ainda usa server keys precisa migrar para credenciais de service account.
  • Plugin.Firebase 3.x é a opção mais madura para .NET MAUI 9/10; Shiny.Push é a alternativa quando precisa de abstração multi-provedor.
  • No iOS, notificações de dados puros (silent push) exigem content-available: 1 e a capability "Remote notifications". Sem isso, o sistema bloqueia entregas.
  • Android 13+ exige permissão runtime POST_NOTIFICATIONS; pedir esse consentimento no momento errado derruba a taxa de opt-in em 30%.
  • Deep linking funciona via NotificationResponse + Shell navigation; resolver a rota antes do OnStart evita race conditions.
  • Azure Notification Hubs ainda compensa quando precisa unificar segmentação cross-platform sem manter sua própria tabela de tokens.

Arquitetura: como push funciona no .NET MAUI

Antes de qualquer linha de código, vale entender o fluxo. Push no .NET MAUI nunca é uma única peça: são três sistemas a conversar. O app pede um token ao serviço da plataforma (FCM no Android, APNs no iOS), envia esse token para o seu backend, e o backend usa-o como endereço quando quer empurrar uma mensagem. A plataforma entrega ao dispositivo, o sistema operativo desperta o processo do app, e o seu código tem alguns segundos para reagir.

Honestamente, há duas categorias de mensagem que confundem quem está a começar. Notification messages são entregues pelo sistema mesmo com o app morto (aparecem na barra automaticamente). Já as data messages só são tratadas pelo seu código e exigem que o processo esteja vivo, ou que use background handlers. No iOS, mensagens silenciosas só passam se incluir content-available: 1 no payload e tiver activado a capability "Background Modes → Remote notifications". Já vi equipas inteiras a perderem dias por não saberem isto.

No lado .NET, a camada certa para tocar é a Platform integration do .NET MAUI: handlers parciais por plataforma e MauiProgram.CreateMauiApp para registar serviços. Tudo o resto é wrapper.

Configurar Firebase Cloud Messaging no Android

Comece pelo Firebase Console: crie um projeto, adicione um app Android com o seu applicationId exacto (tem de bater certo com o do csproj), e baixe o google-services.json. Coloque-o em Platforms/Android/google-services.json e marque como GoogleServicesJson no .csproj:

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

No AndroidManifest.xml, declare o serviço de mensagens. A partir do Android 13 (API 33) precisa também da permissão runtime:

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

<application>
  <meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="default_channel" />
</application>

Crie um canal de notificação no arranque (obrigatório no Android 8+):

var channel = new NotificationChannel(
    "default_channel",
    "Notificações Gerais",
    NotificationImportance.High);
var manager = (NotificationManager)GetSystemService(NotificationService);
manager?.CreateNotificationChannel(channel);

Configurar APNs no iOS com .NET MAUI

Do lado iOS, há mais burocracia. No Apple Developer portal, active "Push Notifications" no seu App ID e gere uma chave APNs (.p8). A chave é por equipa, serve todos os apps, e não expira. Guarde-a no servidor backend, nunca no app.

No projeto MAUI, edite o Entitlements.plist em Platforms/iOS:

<key>aps-environment</key>
<string>development</string>

Mude para production ao publicar. Esquecer isto faz o app receber tokens válidos só em sandbox, e os pushes de produção falham silenciosamente. Eu próprio perdi uma tarde inteira com este detalhe num release há uns meses. No csproj do iOS, referencie o entitlements:

<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))">
  <CodesignEntitlements>Platforms/iOS/Entitlements.plist</CodesignEntitlements>
</PropertyGroup>

No Info.plist, se precisar de silent push, declare o background mode:

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

Plugin.Firebase: implementação prática para .NET MAUI

O Plugin.Firebase.CloudMessaging abstrai o registo, recepção e o ciclo de vida em ambas plataformas. Adicione ao projeto:

<PackageReference Include="Plugin.Firebase.CloudMessaging" Version="3.1.0" />
<PackageReference Include="Plugin.Firebase.Bundled" Version="3.1.0" />

Em MauiProgram.cs, registe o serviço durante o builder:

builder
    .UseMauiApp<App>()
    .RegisterFirebaseServices()
    .Services.AddSingleton(CrossFirebaseCloudMessaging.Current);

builder.Services.AddSingleton<IPushNotificationService, PushNotificationService>();

A interface que recomendo isolar (fica fácil de mockar em testes e substituir o provedor mais tarde):

public interface IPushNotificationService
{
    Task<string?> RegisterAsync(CancellationToken ct = default);
    event EventHandler<PushPayload> MessageReceived;
    event EventHandler<PushPayload> NotificationTapped;
}

public record PushPayload(string Title, string Body, IDictionary<string, string> Data);

public sealed class PushNotificationService : IPushNotificationService
{
    private readonly IFirebaseCloudMessaging _fcm;

    public event EventHandler<PushPayload>? MessageReceived;
    public event EventHandler<PushPayload>? NotificationTapped;

    public PushNotificationService(IFirebaseCloudMessaging fcm)
    {
        _fcm = fcm;
        _fcm.NotificationReceived += (s, e) =>
            MessageReceived?.Invoke(this, ToPayload(e.Notification));
        _fcm.NotificationTapped += (s, e) =>
            NotificationTapped?.Invoke(this, ToPayload(e.Notification));
    }

    public async Task<string?> RegisterAsync(CancellationToken ct = default)
    {
        await _fcm.CheckIfValidAsync();
        var granted = await _fcm.RequestNotificationPermissionAsync();
        if (!granted) return null;

        return await _fcm.GetTokenAsync();
    }

    private static PushPayload ToPayload(FCMNotification n) =>
        new(n.Title ?? string.Empty,
            n.Body ?? string.Empty,
            n.Data ?? new Dictionary<string, string>());
}

No primeiro lançamento depois do utilizador aceitar a permissão, chame RegisterAsync e envie o token ao backend. Trate refresh: o token pode mudar (reinstalar app, limpar dados, restore). O Plugin.Firebase dispara TokenRefreshed; encaminhe sempre para o seu endpoint.

Enviando push pelo backend com FCM HTTP v1

A API legacy do FCM foi descontinuada em junho de 2024. Toda a comunicação server-to-FCM agora passa por OAuth 2.0 com credenciais de service account. No backend (assumindo ASP.NET Core), use o FirebaseAdmin SDK:

FirebaseApp.Create(new AppOptions
{
    Credential = GoogleCredential.FromFile("service-account.json")
});

public async Task SendAsync(string token, string title, string body, IDictionary<string, string> data)
{
    var message = new Message
    {
        Token = token,
        Notification = new Notification { Title = title, Body = body },
        Data = data,
        Apns = new ApnsConfig
        {
            Aps = new Aps { ContentAvailable = true, Sound = "default" }
        },
        Android = new AndroidConfig
        {
            Priority = Priority.High,
            Notification = new AndroidNotification { ChannelId = "default_channel" }
        }
    };

    await FirebaseMessaging.DefaultInstance.SendAsync(message);
}

Note como o mesmo SDK trata APNs por baixo. A Apple aceita pushes encaminhados pelo Firebase desde que tenha colado a chave .p8 no Firebase Console (Project settings → Cloud Messaging → Apple app configuration). Isto poupa-lhe manter dois pipelines.

Deep linking e navegação Shell ao tocar na notificação

O ponto onde mais apps tropeçam: o utilizador toca na notificação, o app abre, e cai sempre na home, ignorando o conteúdo. A solução é incluir uma rota no payload e processá-la depois do Shell estar pronto.

O backend envia algo como "route": "//orders/12345" no campo data. No app, escute NotificationTapped:

public partial class App : Application
{
    private readonly IPushNotificationService _push;

    public App(IPushNotificationService push)
    {
        InitializeComponent();
        _push = push;
        MainPage = new AppShell();

        _push.NotificationTapped += async (s, payload) =>
        {
            if (payload.Data.TryGetValue("route", out var route))
            {
                await Shell.Current.Dispatcher.DispatchAsync(async () =>
                    await Shell.Current.GoToAsync(route));
            }
        };
    }
}

Se o app foi aberto a partir da notificação (cold start), o evento dispara antes do AppShell existir. Guarde o payload num campo e processe-o em OnStart. Esta lógica beneficia de uma camada MVVM bem isolada. Se ainda não migrou para o padrão de geração de código, vale a pena ler o nosso guia de MVVM com CommunityToolkit no .NET MAUI antes.

Background handlers e silent push

Quando precisa de actualizar dados sem chamar a atenção do utilizador (sincronizar inbox, refrescar tokens de auth, fazer download de conteúdo), use silent push. O payload contém apenas data, sem notification, e o iOS exige content-available: 1.

No Android, o Plugin.Firebase já encaminha mensagens de dados para o seu handler mesmo com o app em background. Mas se a mensagem chegar com o processo terminado, só sobrevive se enviar com priority: high. No iOS, o sistema dá 30 segundos para o handler terminar antes de suspender o processo:

_push.MessageReceived += async (s, payload) =>
{
    if (payload.Data.TryGetValue("sync", out var syncType))
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(25));
        try { await _syncService.RunAsync(syncType, cts.Token); }
        catch (OperationCanceledException) { }
    }
};

Atenção: o iOS aplica um budget agressivo a silent push. Mais de 2-3 por hora por device e o sistema começa a descartar entregas. Para sincronização frequente, prefira o Background Tasks framework agendado, não push.

Como testar e debugar notificações push em .NET MAUI

O ciclo de feedback aqui é o maior assassino de produtividade: push falha sem erro, sem log, sem nada. Algumas técnicas que poupam horas:

  • FCM Console → Cloud Messaging → Send test message: cole o token do device e envie. Se chegar aqui mas não pelo seu backend, o problema é no servidor.
  • Apple Push Notification Console (developer.apple.com): mesma lógica, mas para APNs directo. Útil para confirmar que o certificado/chave está correto.
  • iOS Simulator + .apns file: a partir do Xcode 14, arrastar um JSON com extensão .apns para o simulador entrega push fake. Cria um loop rápido para testar UI sem device.
  • adb logcat -s FA FA-SVC FirebaseMessaging: mostra o que o FCM realmente recebeu no Android.
  • Console.app + filtro "apsd": no Mac, mostra o tráfego APNs do iPhone conectado.

Para testes automatizados, mocke a IPushNotificationService e dispare eventos fake. Isto cobre lógica de navegação e UI sem precisar de infra real. A camada de testes geral vale a pena estruturar bem; o nosso guia completo de testes em .NET MAUI mostra como organizar unit, UI e integração.

Quando usar Azure Notification Hubs em vez de FCM directo

CritérioFCM directoAzure Notification Hubs
Custo inicialGrátis até 1M msgs/diaFree tier até 1M push/mês
Multi-plataformaAndroid + iOS via FCMFCM, APNs, WNS, Baidu (unificados)
Segmentação por tagsManual (você mantém a tabela)Built-in, queries com expressões
Gestão de tokensVocê gere expiração e refreshHub trata installations
ThroughputLimitado por quota FCMStandard tier escala para milhões/min
Vendor lock-inBaixoMédio (formato de installation Azure-específico)

Para apps abaixo de ~50 mil utilizadores activos, FCM directo é mais simples e barato. Acima disso, ou quando precisa de enviar para a China (Baidu) ou Windows desktop, o Notification Hubs paga-se em produtividade. Eu uso Hubs sempre que a aplicação tem segmentação rica (tipo "notificar todos os utilizadores em SP que tenham activado promoções"). Montar essa query manualmente contra uma tabela de tokens não escala.

Se a sua arquitectura backend já usa Aspire para orquestração local, vale ler também o nosso artigo sobre integração de .NET MAUI com .NET Aspire. O Notification Hubs encaixa naturalmente como recurso Aspire.

Perguntas Frequentes

O .NET MAUI suporta notificações push nativamente?

Não directamente. O .NET MAUI fornece os hooks de plataforma (parciais por target), mas a recepção e envio passam pelos serviços nativos: FCM no Android e APNs no iOS. Pacotes como Plugin.Firebase.CloudMessaging ou Shiny.Push embrulham essas APIs num único contrato C#.

Qual a diferença entre notificações locais e push no .NET MAUI?

Notificações locais são agendadas pelo próprio app e disparam sem precisar de internet, ideais para lembretes baseados em tempo ou localização. Push notifications vêm de um servidor remoto via FCM/APNs e funcionam mesmo com o app fechado, ao custo de exigir registo de token e backend.

Como testar push notifications no simulador iOS?

A partir do iOS 16 com Xcode 14+, arraste um ficheiro .apns (JSON com payload APNs e header Simulator Target Bundle) para a janela do simulador. Para silent push, inclua content-available: 1 no aps. Não substitui testes em device real, mas é suficiente para iterar lógica de UI.

Por que minhas push notifications não chegam quando o app está fechado no iOS?

A causa mais comum é faltar a capability "Remote notifications" em Background Modes do Info.plist, ou enviar com prioridade normal. Para data-only messages, o iOS exige content-available: 1 e prioridade 5; sem isso, o sistema descarta. Verifique também se o entitlement aps-environment está como production em builds de release.

Plugin.Firebase ou Shiny.Push: qual escolher para .NET MAUI?

Plugin.Firebase é mais maduro, com integração directa ao Firebase Console e suporte para Analytics, Crashlytics e Auth no mesmo pacote, ideal se já está no ecossistema Firebase. Shiny.Push abstrai múltiplos provedores (FCM, APNs, Azure, native) atrás de uma interface única, útil quando precisa de flexibilidade ou está em Notification Hubs. Em apps que só usam Firebase, prefiro Plugin.Firebase pela menor superfície de configuração.

Marcus Chen
Sobre o Autor Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.