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 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 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 e as diretrizes de notificações do Android 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 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 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çaPapel
Token FCMendereço lógico do app nesta instalação
Tópico / condiçãosegmentação no servidor sem listar tokens um a um
FirebaseMessagingServicerecebe mensagens no processo do app
NotificationChannelcategoria, importância, som e agrupamento no sistema
Payload notificationo sistema pode exibir sozinho em background
Payload dataseu código decide o que fazer
PendingIntent / deep linkpara onde o usuário vai ao tocar
Preferências do produtoo 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:

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 ou o Application padrão. O importante é não bloquear a main thread com trabalho pesado no boot.

Token FCM: obter, enviar e renovar

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 e a LGPD no Android 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):

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 já cobre o padrão Activity Result + Compose; aqui o recorte específico de push:

@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:

TipoApp em foregroundApp em background / morto
notificationseu onMessageReceived costuma rodar; você decide exibiro sistema pode exibir a notificação automaticamente
datavocê decide tudovocê decide tudo (com limites de execução)
híbridocomportamento depende da plataforma e do estadoo 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:

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 e Firebase Performance.

Exibir a notificação e abrir a tela certa

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 e App Links / deep links. 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, captura contínua com CameraX. 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 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.

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).

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 flavorsgoogle-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, vagas em kotlin.dev.br/vagas e um app amostra que registre o token em um backend Kotlin (Spring ou Ktor).

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 e o momento do POST_NOTIFICATIONS;
  2. enfileirar trabalho pós-push com WorkManager;
  3. abrir a tela certa via App Links;
  4. observar estabilidade com Crashlytics e Performance Monitoring;
  5. endurecer fluxos sensíveis com Play Integrity e BiometricPrompt.

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.