---
title: "FCM e notificações push no Android com Kotlin e Compose em 2026"
url: "https://kotlin.dev.br/blog/fcm-notificacoes-push-android-kotlin-compose-2026/"
markdown_url: "https://kotlin.dev.br/blog/fcm-notificacoes-push-android-kotlin-compose-2026.MD"
description: "Implemente FCM no Android com Kotlin: tokens, NotificationChannel, POST_NOTIFICATIONS, payload data vs notification, deep link, Compose e privacidade."
date: "2026-08-31"
author: "Karina Melo"
---

# FCM e notificações push no Android com Kotlin e Compose em 2026

Implemente FCM no Android com Kotlin: tokens, NotificationChannel, POST_NOTIFICATIONS, payload data vs notification, deep link, Compose e privacidade.


**Resposta rápida:** use **Firebase Cloud Messaging (FCM)** quando o servidor precisa avisar o app Android — pedido atualizado, mensagem nova, alerta de segurança, lembrete ou sincronização. No Kotlin moderno, separe três camadas: (1) obter e renovar o **token FCM** com segurança; (2) criar **NotificationChannel** adequados e respeitar `POST_NOTIFICATIONS` no Android 13+; (3) decidir o que fazer com o payload — exibir notificação, abrir deep link, enfileirar [WorkManager](/blog/workmanager-kotlin-android-2026/) ou atualizar estado local. Em Compose, trate a permissão e as preferências do produto como estado de UI, não como detalhe escondido no `Application`. FCM não substitui um canal de tempo real bidirecional (WebSocket/SSE) nem justifica um foreground service permanente sem necessidade clara.

Push notification parece “só mostrar um balão” até o app cair em produção. O token muda, o usuário nega a permissão, o canal fica silencioso demais, o payload chega em background e some, a tela errada abre no clique, ou o time usa WorkManager periódico tentando fingir tempo real. O [roadmap Android](/carreira/roadmap-dev-android/) já aponta FCM como habilidade esperada; este guia fecha a lacuna com um caminho prático em Kotlin.

Consulte também a [documentação oficial do Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) e as [diretrizes de notificações do Android](https://developer.android.com/develop/ui/views/notifications) antes de fixar versões do BoM Firebase e das APIs de notificação do sistema.

## Quando FCM vale a pena — e quando não

FCM é a escolha certa quando:

- o **servidor** precisa iniciar a conversa com o dispositivo;
- a mensagem pode chegar com o app em background ou morto;
- você precisa de fan-out (um para muitos) ou targeting por tópico;
- o produto depende de alertas transacionais (pagamento, entrega, segurança).

Evite FCM quando:

- a interação é só local (alarme de pomodoro, lembrete criado no aparelho) — use `AlarmManager` / agendamento local;
- você precisa de streaming contínuo bidirecional — prefira WebSocket, [SSE no Ktor](/blog/ktor-sse-server-sent-events-kotlin-2026/) ou canal próprio;
- a “notificação” é só marketing agressivo sem valor — isso queima permissão e retenção.

O erro clássico documentado no guia de [WorkManager](/blog/workmanager-kotlin-android-2026/) continua válido: **não use periodic work para simular push**. Bateria, Doze e quotas vão atrasar o job; FCM existe justamente para esse sinal.

## Arquitetura mental: token, canal, payload e clique

| Peça | Papel |
|---|---|
| Token FCM | endereço lógico do app nesta instalação |
| Tópico / condição | segmentação no servidor sem listar tokens um a um |
| `FirebaseMessagingService` | recebe mensagens no processo do app |
| `NotificationChannel` | categoria, importância, som e agrupamento no sistema |
| Payload `notification` | o sistema pode exibir sozinho em background |
| Payload `data` | seu código decide o que fazer |
| PendingIntent / deep link | para onde o usuário vai ao tocar |
| Preferências do produto | o usuário controla tipos de alerta além da permissão do SO |

Em times sérios, o **backend** é dono do envio. O app Android autentica o usuário, envia o token atualizado e trata a mensagem. Não embuta chaves de servidor FCM no APK.

## Dependências e setup mínimo

No módulo `:app`, alinhe o Firebase BoM e o messaging:

```kotlin
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
    implementation("com.google.firebase:firebase-messaging-ktx")
}
```

Confirme a versão estável atual do BoM na documentação do Firebase antes de piná-la. Aplique o plugin do Google Services no Gradle do app e coloque o `google-services.json` correto por flavor (dev/staging/prod). Tokens de debug misturados com produção são uma fonte clássica de “push não chega”.

Inicialize o Firebase cedo — muitas bases já usam [AndroidX App Startup](/blog/androidx-startup-kotlin-inicializacao-app-2026/) ou o `Application` padrão. O importante é não bloquear a main thread com trabalho pesado no boot.

## Token FCM: obter, enviar e renovar

```kotlin
class PushTokenRepository(
    private val messaging: FirebaseMessaging,
    private val api: DeviceApi,
) {
    suspend fun syncToken() {
        val token = messaging.token.await()
        api.registerDeviceToken(token)
    }
}
```

Trate três eventos:

1. **primeiro login / instalação** — sincroniza o token;
2. **`onNewToken`** — o FCM rotacionou o endereço;
3. **logout** — desassocie o token do usuário no backend (e considere `deleteToken()` se a política de privacidade exigir).

Guarde o token com o mesmo cuidado de outros identificadores locais. O guia de [segurança de dados locais](/blog/seguranca-dados-locais-android-kotlin-2026/) e a [LGPD no Android](/blog/lgpd-android-kotlin-2026/) ajudam a decidir o que fica em DataStore, o que vai cifrado e o que nunca deve ir para analytics.

## NotificationChannel: sem canal, não há produto

Desde o Android 8, canais não são opcionais. Crie-os no startup (idempotente):

```kotlin
fun ensureChannels(context: Context) {
    val manager = context.getSystemService(NotificationManager::class.java)

    val transactional = NotificationChannel(
        "orders",
        "Pedidos e entregas",
        NotificationManager.IMPORTANCE_HIGH,
    ).apply {
        description = "Atualizações de pedido, pagamento e entrega"
    }

    val social = NotificationChannel(
        "social",
        "Mensagens e menções",
        NotificationManager.IMPORTANCE_DEFAULT,
    )

    val marketing = NotificationChannel(
        "marketing",
        "Novidades e ofertas",
        NotificationManager.IMPORTANCE_LOW,
    )

    manager.createNotificationChannels(listOf(transactional, social, marketing))
}
```

Boas práticas:

- **um canal por intenção de produto**, não um canal genérico “Geral”;
- importância alta só para o que realmente interrompe;
- nomes e descrições em PT-BR claros — o usuário gerencia isso nas configurações do sistema;
- nunca mude o **ID** do canal depois do lançamento se quiser preservar a escolha do usuário.

## Permissão `POST_NOTIFICATIONS` e Compose

No Android 13+, exibir notificações exige permissão em runtime. O guia de [permissões Android](/blog/permissoes-android-kotlin-2026/) já cobre o padrão Activity Result + Compose; aqui o recorte específico de push:

```kotlin
@Composable
fun NotificationPermissionCard(
    onRequest: () -> Unit,
    onOpenSettings: () -> Unit,
) {
    Card {
        Text("Ative alertas de pedido para saber quando sair para entrega.")
        Button(onClick = onRequest) { Text("Permitir notificações") }
        TextButton(onClick = onOpenSettings) { Text("Abrir configurações") }
    }
}
```

Regras de produto que salvam conversão:

- peça a permissão **no momento de valor** (ativar alertas de pedido), não no primeiro frame do onboarding;
- explique o benefício antes do dialog do sistema;
- ofereça toggles internos por tipo (transacional vs marketing) mesmo depois da permissão concedida;
- se o usuário negou duas vezes, encaminhe para as configurações do app em vez de spammar o dialog.

## Payload: `notification` vs `data`

FCM aceita mensagens com bloco `notification`, bloco `data`, ou ambos. A diferença prática:

| Tipo | App em foreground | App em background / morto |
|---|---|---|
| só `notification` | seu `onMessageReceived` costuma rodar; você decide exibir | o sistema pode exibir a notificação automaticamente |
| só `data` | você decide tudo | você decide tudo (com limites de execução) |
| híbrido | comportamento depende da plataforma e do estado | o sistema pode mostrar o bloco notification e entregar data no Intent do clique |

Para apps com lógica de negócio (deeplink tipado, dedupe, analytics, escrita local), muitos times preferem **data messages** controladas pelo app, com o backend enviando título/corpo/rota como chaves. O trade-off é mais código e mais responsabilidade sobre exibir a notificação corretamente.

Exemplo de serviço enxuto:

```kotlin
class AppFirebaseMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        // Enfileire sync do token — não faça rede bloqueante aqui
        PushTokenWorker.enqueue(applicationContext, token)
    }

    override fun onMessageReceived(message: RemoteMessage) {
        val data = message.data
        val type = data["type"].orEmpty()
        when (type) {
            "order_update" -> OrderPushHandler.handle(applicationContext, data)
            "chat_message" -> ChatPushHandler.handle(applicationContext, data)
            else -> GenericPushHandler.handle(applicationContext, message)
        }
    }
}
```

Mantenha `onMessageReceived` curto. Se precisar baixar anexo, reconciliar banco ou chamar API, delegue para WorkManager com constraints. Isso evita ANR e combina com o que o time já pratica em [Crashlytics/ANR](/blog/firebase-crashlytics-anr-android-kotlin-2026/) e [Firebase Performance](/blog/firebase-performance-monitoring-android-kotlin-2026/).

## Exibir a notificação e abrir a tela certa

```kotlin
fun showOrderNotification(context: Context, orderId: String, title: String, body: String) {
    val deepLink = "https://app.exemplo.com.br/pedidos/$orderId".toUri()
    val intent = Intent(Intent.ACTION_VIEW, deepLink).apply {
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
    }
    val pending = PendingIntent.getActivity(
        context,
        orderId.hashCode(),
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
    )

    val notification = NotificationCompat.Builder(context, "orders")
        .setSmallIcon(R.drawable.ic_notification)
        .setContentTitle(title)
        .setContentText(body)
        .setContentIntent(pending)
        .setAutoCancel(true)
        .setPriority(NotificationCompat.PRIORITY_HIGH)
        .build()

    NotificationManagerCompat.from(context).notify(orderId.hashCode(), notification)
}
```

Conecte o clique ao mesmo grafo de navegação que você já usa com [Navigation Compose](/blog/navigation-compose-navhost-navcontroller-kotlin-2026/) e [App Links / deep links](/blog/app-links-deep-links-android-kotlin-2026/). Push que abre a home genérica destrói a confiança do usuário.

## Foreground service: use com justificativa

Há cenários legítimos — navegação turn-by-turn, upload longo visível, sessão de mídia com [Media3](/blog/media3-exoplayer-compose-kotlin-2026/), captura contínua com [CameraX](/blog/camerax-compose-kotlin-2026/). Nesses casos o Android exige notificação persistente e, em versões recentes, `foregroundServiceType` declarado no manifesto.

Não transforme todo push em foreground service. Para sync curto após um data message, WorkManager + notificação cancelável costuma ser suficiente e mais amigável à bateria. Se o Play Console pedir declaração de tipo de service, alinhe o manifesto à realidade do produto — declarações falsas geram rejeição.

## Segurança, abuso e Play Integrity

Push é vetor de phishing interno se o backend for frágil:

- autentique o registro de token (usuário logado, device id estável, ambiente);
- assine ou valide no servidor qualquer ação sensível disparada pelo clique;
- não confie em deep links abertos sem checagem (open redirect, orderId de outro usuário);
- rate-limite envios por usuário/dispositivo;
- para fluxos de alto risco (pagamento, troca de senha), combine com [Play Integrity](/blog/play-integrity-api-android-kotlin-2026/) e confirmação in-app — a notificação avisa, a ação crítica confirma.

Biometria e Keystore entram quando o clique leva a uma ação sensível; veja [BiometricPrompt](/blog/biometricprompt-android-kotlin-biometria-2026/).

## Testes e diagnóstico

Checklist mínimo:

1. token sincroniza após login e após `onNewToken`;
2. canais existem logo na primeira abertura;
3. permissão Android 13+ pedida no momento certo;
4. data message em foreground atualiza UI ou enfileira worker;
5. app morto ainda mostra notificação e abre a rota correta;
6. logout remove associação do token;
7. marketing respeita opt-out interno mesmo com permissão do SO;
8. não há PII sensível no título/corpo visível na lock screen sem necessidade.

No Logcat, filtre por `FirebaseMessaging` e pelo seu `tag` de handler. No console Firebase, use o envio de teste para o token do device de desenvolvimento. Em CI, prefira testes unitários dos parsers de payload e testes instrumentados do PendingIntent / deep link — Espresso ajuda na navegação pós-clique ([guia Espresso](/blog/espresso-android-kotlin-testes-ui-2026/)).

## Erros comuns

- **Pedir permissão no launch frio** — o usuário nega e o canal de receita some.
- **Um único canal “Geral”** — o usuário silencia tudo por causa de promoção.
- **Chave de servidor no app** — qualquer APK descompilado envia spam em seu nome.
- **Ignorar `onNewToken`** — metade da base para de receber push após reinstall/backup.
- **Payload gigante** — FCM não é CDN; mande IDs e busque o detalhe na API.
- **WorkManager “quase em tempo real”** — atrasa, drena confiança e não substitui FCM.
- **Abrir sempre a MainActivity** — deep link quebrado = suporte lotado.
- **Misturar flavors** — `google-services.json` de prod em build de debug (ou o inverso).

## Onde encaixar na carreira Android

Em entrevistas e portfólio, um fluxo FCM completo demonstra maturidade de produto: permissões, canais, ciclo de token, payload, navegação e privacidade. Combine com o [roadmap Android](/carreira/roadmap-dev-android/), vagas em [kotlin.dev.br/vagas](/vagas/) e um app amostra que registre o token em um backend Kotlin ([Spring](/blog/kotlin-spring-boot/) ou [Ktor](/blog/ktor-criando-apis-kotlin/)).

## Perguntas frequentes

### FCM funciona sem Google Play Services?

Em dispositivos sem Play Services o cliente oficial do FCM não é o caminho padrão. Avalise a matriz de dispositivos do produto (Android puro, Huawei, etc.) antes de prometer push universal. Para a maioria dos apps brasileiros na Play Store, FCM permanece o padrão.

### Qual a diferença entre tópico e token?

Token endereça uma instalação. Tópico endereça um conjunto de assinantes (`news`, `order_123` raramente faz sentido como tópico eterno). Use tópicos para fan-out amplo; use tokens (ou groups no backend) para mensagens pessoais.

### Posso garantir entrega em tempo real absoluto?

Não. Rede, Doze, otimização de bateria do OEM e decisão do usuário afetam a entrega. Desenhe o produto para ser eventualmente consistente: a notificação é um sinal; a fonte da verdade continua sendo a API ao abrir o app.

### Notificação local e push são a mesma API de UI?

A exibição usa as mesmas `NotificationCompat` / canais. A origem muda: local nasce no aparelho; push nasce no servidor via FCM. Reaproveite o builder e os canais; separe os disparadores.

### Preciso de Compose obrigatoriamente?

Não. FCM e canais são APIs de plataforma/serviço. Compose brilha na UX de permissão e preferências. Views tradicionais continuam válidas se o restante do app ainda não migrou.

## Conclusão e próximos passos

Push bem feito é menos “biblioteca Firebase” e mais **contrato entre backend, sistema operacional e expectativa do usuário**. Com Kotlin, mantenha o `FirebaseMessagingService` fino, canais honestos, permissão contextual, payload previsível e deep links testados.

Próximos passos naturais no cluster Android deste site:

1. revisar [permissões](/blog/permissoes-android-kotlin-2026/) e o momento do `POST_NOTIFICATIONS`;
2. enfileirar trabalho pós-push com [WorkManager](/blog/workmanager-kotlin-android-2026/);
3. abrir a tela certa via [App Links](/blog/app-links-deep-links-android-kotlin-2026/);
4. observar estabilidade com [Crashlytics](/blog/firebase-crashlytics-anr-android-kotlin-2026/) e [Performance Monitoring](/blog/firebase-performance-monitoring-android-kotlin-2026/);
5. endurecer fluxos sensíveis com [Play Integrity](/blog/play-integrity-api-android-kotlin-2026/) e [BiometricPrompt](/blog/biometricprompt-android-kotlin-biometria-2026/).

Se você está montando portfólio, documente no README do app: como obter o token de debug, quais canais existem, e como o backend autentica o registro. Isso diferencia um demo de um app pronto para operação.
