Receber mensagens em um app do Flutter

Dependendo do estado do dispositivo, as mensagens recebidas serão processadas de forma diferente. Para entender esses cenários e saber como integrar o FCM ao seu próprio aplicativo, primeiro é importante definir os vários estados em que um dispositivo pode estar:

Estado Descrição
Primeiro plano Quando o aplicativo está aberto, em visualização e em uso.
Contexto Quando o aplicativo está aberto, mas em segundo plano (minimizado). Isso geralmente ocorre quando o usuário pressiona o botão "Início" no dispositivo, muda para outro app usando o seletor ou abre o app em outra guia (Web).
Encerrado Quando o dispositivo está bloqueado ou o app não está em execução.

Há algumas pré-condições que precisam ser atendidas para que o aplicativo possa receber payloads de mensagens pelo FCM:

  • O app precisa ter sido aberto pelo menos uma vez para permitir o registro no FCM.
  • No iOS, se o usuário deslizar o aplicativo do seletor de apps, ele precisará ser reaberto manualmente para que as mensagens em segundo plano comecem a funcionar novamente.
  • No Android, se o usuário forçar o encerramento do app nas configurações do dispositivo, ele precisará ser reaberto manualmente para que as mensagens comecem a funcionar.
  • Na Web, você precisa ter solicitado um token (usando getToken()) com seu certificado push da Web.

Solicitar permissão para receber mensagens

No iOS, macOS, na Web e no Android 13 (ou mais recente), antes de receber payloads do FCM no seu dispositivo, você precisa pedir a permissão do usuário.

O pacote firebase_messaging fornece uma API simples para solicitar permissão pelo método requestPermission. Essa API aceita vários argumentos nomeados que definem o tipo de permissão que você quer solicitar, como se as mensagens que contêm payloads de notificação podem acionar um som ou ler mensagens pela Siri. Por padrão, o método solicita permissões padrão simples. A API de referência fornece uma documentação completa sobre a finalidade de cada permissão.

Para começar, chame o método do seu aplicativo. No iOS, um modal nativo será exibido. Na Web, o fluxo de API nativo do navegador será acionado:

FirebaseMessaging messaging = FirebaseMessaging.instance;

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

print('User granted permission: ${settings.authorizationStatus}');

A propriedade authorizationStatus do objeto NotificationSettings retornada da solicitação pode ser usada para determinar a decisão geral do usuário:

  • authorized: o usuário concedeu permissão.
  • denied: o usuário negou a permissão.
  • notDetermined: o usuário ainda não escolheu se quer conceder permissão.
  • provisional: o usuário concedeu permissão provisória

As outras propriedades no NotificationSettings retornam se uma permissão específica está ativada, desativada ou não é compatível com o dispositivo atual.

Depois que a permissão for concedida e os diferentes tipos de estado do dispositivo forem compreendidos, seu aplicativo poderá começar a processar os payloads do FCM recebidos.

Gerenciamento de mensagens

Com base no estado atual do seu aplicativo, os payloads de entrada de diferentes tipos de mensagens exigem diferentes implementações para processá-las:

Mensagens em primeiro plano

Para processar mensagens enquanto seu aplicativo está em primeiro plano, ouça o stream onMessage.

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print('Got a message whilst in the foreground!');
  print('Message data: ${message.data}');

  if (message.notification != null) {
    print('Message also contained a notification: ${message.notification}');
  }
});

O stream contém um RemoteMessage, detalhando várias informações sobre o payload, como a origem, o ID exclusivo, o horário de envio, se ele continha uma notificação e muito mais. Como a mensagem foi recuperada enquanto o aplicativo estava em primeiro plano, é possível acessar diretamente o estado e o contexto do app do Flutter.

Mensagens em primeiro plano e de notificação

As mensagens de notificação que chegam enquanto o aplicativo está em primeiro plano não exibem uma notificação visível por padrão no Android e no iOS. No entanto, é possível substituir esse comportamento:

  • No Android, você precisa criar um canal de notificação de "Alta prioridade".
  • No iOS, você pode atualizar as opções de apresentação do aplicativo.

Mensagens em segundo plano

O processo de processamento de mensagens em segundo plano é diferente em plataformas nativas (Android e Apple) e baseadas na Web.

Plataformas Apple e Android

Gerencie mensagens em segundo plano registrando um gerenciador onBackgroundMessage. Quando as mensagens são recebidas, uma mensagem isolado é gerada (somente Android, iOS/macOS não requer um isolamento separado), permitindo que você lide com mensagens mesmo quando seu aplicativo não estiver em execução.

Há alguns detalhes a serem considerados sobre o gerenciador de mensagens em segundo plano:

  1. Não pode ser uma função anônima.
  2. Ele precisa ser uma função de nível superior (por exemplo, não é um método de classe que requer inicialização).
  3. Ao usar o Flutter 3.3.0 ou uma versão mais recente, o gerenciador de mensagens precisa ter a anotação @pragma('vm:entry-point') logo acima da declaração da função. Caso contrário, ele será removido durante o tree shaking para o modo de lançamento.
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp();

  print("Handling a background message: ${message.messageId}");
}

void main() {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}

Como o gerenciador é executado no próprio isolamento fora do contexto dos aplicativos, não é possível atualizar o estado do app nem executar qualquer lógica que afete a IU. No entanto, é possível executar lógica como solicitações HTTP, executar operações de E/S (por exemplo, atualizar o armazenamento local), se comunicar com outros plug-ins etc.

Também é recomendável concluir sua lógica assim que possível. A execução de tarefas longas e intensas afeta o desempenho do dispositivo e pode fazer com que o SO encerre o processo. Se as tarefas durarem mais de 30 segundos, o dispositivo poderá encerrar o processo automaticamente.

Web

Na Web, escreva um service worker em JavaScript, que será executado em segundo plano. Usar o service worker para processar mensagens em segundo plano.

Para começar, crie um novo arquivo no diretório web e chame-o de firebase-messaging-sw.js:

// Please see this file for the latest firebase-js-sdk version:
// https://github.com/firebase/flutterfire/blob/master/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js");

firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
});

const messaging = firebase.messaging();

// Optional:
messaging.onBackgroundMessage((message) => {
  console.log("onBackgroundMessage", message);
});

O arquivo precisa importar os SDKs do app e das mensagens, inicializar o Firebase e expor a variável messaging.

Em seguida, o worker precisa ser registrado. No arquivo index.html, registre o worker modificando a tag <script> que inicializa o Flutter:

<script src="flutter_bootstrap.js" async>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
      navigator.serviceWorker.register('firebase-messaging-sw.js', {
        scope: '/firebase-cloud-messaging-push-scope',
      });
    });
  }
</script>

Se você ainda estiver usando o antigo sistema de modelos, poderá registrar o worker modificando a tag <script>, que inicializa o Flutter, da seguinte maneira:

<html>
<body>
  <script>
      var serviceWorkerVersion = null;
      var scriptLoaded = false;
      function loadMainDartJs() {
        if (scriptLoaded) {
          return;
        }
        scriptLoaded = true;
        var scriptTag = document.createElement('script');
        scriptTag.src = 'main.dart.js';
        scriptTag.type = 'application/javascript';
        document.body.append(scriptTag);
      }

      if ('serviceWorker' in navigator) {
        // Service workers are supported. Use them.
        window.addEventListener('load', function () {
          // Register Firebase Messaging service worker.
          navigator.serviceWorker.register('firebase-messaging-sw.js', {
            scope: '/firebase-cloud-messaging-push-scope',
          });

          // Wait for registration to finish before dropping the <script> tag.
          // Otherwise, the browser will load the script multiple times,
          // potentially different versions.
          var serviceWorkerUrl =
            'flutter_service_worker.js?v=' + serviceWorkerVersion;

          navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
            function waitForActivation(serviceWorker) {
              serviceWorker.addEventListener('statechange', () => {
                if (serviceWorker.state == 'activated') {
                  console.log('Installed new service worker.');
                  loadMainDartJs();
                }
              });
            }
            if (!reg.active && (reg.installing || reg.waiting)) {
              // No active web worker and we have installed or are installing
              // one for the first time. Simply wait for it to activate.
              waitForActivation(reg.installing ?? reg.waiting);
            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
              // When the app updates the serviceWorkerVersion changes, so we
              // need to ask the service worker to update.
              console.log('New service worker available.');
              reg.update();
              waitForActivation(reg.installing);
            } else {
              // Existing service worker is still good.
              console.log('Loading app from service worker.');
              loadMainDartJs();
            }
          });

          // If service worker doesn't succeed in a reasonable amount of time,
          // fallback to plaint <script> tag.
          setTimeout(() => {
            if (!scriptLoaded) {
              console.warn(
                'Failed to load app from service worker. Falling back to plain <script> tag.'
              );
              loadMainDartJs();
            }
          }, 4000);
        });
      } else {
        // Service workers not supported. Just drop the <script> tag.
        loadMainDartJs();
      }
  </script>
</body>

Em seguida, reinicie o aplicativo do Flutter. O worker vai ser registrado, e todas as mensagens em segundo plano serão gerenciadas por esse arquivo.

Como processar interações

Como as notificações são um sinal visível, é comum que os usuários interajam com elas (pressionando). O comportamento padrão no Android e no iOS é abrir o aplicativo. Se o aplicativo for encerrado, ele vai ser iniciado: caso esteja em segundo plano, o app será colocado no primeiro plano.

Dependendo do conteúdo de uma notificação, recomendamos processar a interação do usuário quando o aplicativo for aberto. Por exemplo, se uma nova mensagem de chat for enviada via notificação, e o usuário tocar nela, recomendamos abrir a conversa específica quando o aplicativo for aberto.

O pacote firebase-messaging oferece duas maneiras de processar essa interação:

  • getInitialMessage(): se o aplicativo for aberto a partir de um estado encerrado, um Future que contém um RemoteMessage será retornado. Depois de consumido, o RemoteMessage será removido.
  • onMessageOpenedApp: um Stream que publica um RemoteMessage quando o aplicativo é aberto a partir de um estado em segundo plano.

É recomendável que os dois cenários sejam processados para garantir uma UX tranquila para os usuários. O exemplo de código abaixo descreve como isso pode ser feito:

class Application extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Application();
}

class _Application extends State<Application> {
  // It is assumed that all messages contain a data field with the key 'type'
  Future<void> setupInteractedMessage() async {
    // Get any messages which caused the application to open from
    // a terminated state.
    RemoteMessage? initialMessage =
        await FirebaseMessaging.instance.getInitialMessage();

    // If the message also contains a data property with a "type" of "chat",
    // navigate to a chat screen
    if (initialMessage != null) {
      _handleMessage(initialMessage);
    }

    // Also handle any interaction when the app is in the background via a
    // Stream listener
    FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
  }

  void _handleMessage(RemoteMessage message) {
    if (message.data['type'] == 'chat') {
      Navigator.pushNamed(context, '/chat',
        arguments: ChatArguments(message),
      );
    }
  }

  @override
  void initState() {
    super.initState();

    // Run code required to handle interacted messages in an async function
    // as initState() must not be async
    setupInteractedMessage();
  }

  @override
  Widget build(BuildContext context) {
    return Text("...");
  }
}

A forma como você interage com as interações depende da configuração do aplicativo. O exemplo acima mostra um caso básico usando um StatefulWidget.

Localizar mensagens

É possível enviar strings localizadas de duas maneiras:

  • armazenar o idioma de preferência de cada um dos seus usuários no seu servidor e enviar notificações personalizadas para cada idioma;
  • incorporar strings localizadas ao app e usar as configurações de localidade nativas do sistema operacional.

Veja como usar o segundo método:

Android

  1. Especifique as mensagens no idioma padrão em resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Especifique as mensagens traduzidas no diretório values-language. Especifique, por exemplo, as mensagens em francês em resources/values-fr/strings.xml:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. Ao invés de usar as chaves title, message e body no payload do servidor, use as chaves title_loc_key e body_loc_key para sua mensagem localizada e defina-as como o atributo name da mensagem que você quer exibir.

    O payload da mensagem seria da seguinte forma:

    {
      "data": {
        "title_loc_key": "notification_title",
        "body_loc_key": "notification_message"
      }
    }
    

iOS

  1. Especifique as mensagens no idioma padrão em Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Especifique as mensagens traduzidas no diretório language.lproj. Especifique, por exemplos, as mensagens em francês em fr.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Bonjour le monde";
    "NOTIFICATION_MESSAGE" = "C'est un message";
    

    O payload da mensagem seria da seguinte forma:

    {
      "data": {
        "title_loc_key": "NOTIFICATION_TITLE",
        "body_loc_key": "NOTIFICATION_MESSAGE"
      }
    }
    

Ativar a exportação de dados de entrega de mensagens

É possível exportar os dados da sua mensagem ao BigQuery para uma análise mais detalhada. Com isso, é possível fazer análises usando o BigQuery SQL, exportar os dados para outro provedor de nuvem ou usá-los nos seus modelos personalizados de ML. A exportação para o BigQuery inclui todos os dados disponíveis para mensagens, independentemente do tipo de mensagem ou se ela é enviada por meio da API ou do Editor do Notificações.

Para ativar a exportação, primeiro siga as etapas descritas aqui e, em seguida, as instruções abaixo:

Android

Use o seguinte código:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

No iOS, é necessário mudar AppDelegate.m com o conteúdo abaixo.

#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
#import <Firebase/Firebase.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  // Override point for customization after application launch.
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
          fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  [[FIRMessaging extensionHelper] exportDeliveryMetricsToBigQueryWithMessageInfo:userInfo];
}

@end

Web

Para Web, é preciso alterar o service worker para usar a versão v9 do SDK. Essa versão precisa estar integrada. Assim sendo, é preciso usar um bundler como esbuild para que o service worker funcione. Consulte o app de exemplo para ver como fazer isso.

Depois de migrar para o SDK v9, use o seguinte código:

import {
  experimentalSetDeliveryMetricsExportedToBigQueryEnabled,
  getMessaging,
} from 'firebase/messaging/sw';
...

const messaging = getMessaging(app);
experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, true);

Execute yarn build para exportar a nova versão do service worker para a pasta web.

Exibir imagens em notificações no iOS

Em dispositivos Apple, para que as notificações de FCM recebidas mostrem imagens da carga útil do FCM, é necessário adicionar uma extensão de serviço de notificação que deve ser configurada para uso do app.

Se você usa a autenticação para smartphones do Firebase, adicione o pod do Firebase Auth ao seu Podfile.

Etapa 1: adicionar uma extensão de serviço de notificação

  1. No Xcode, clique em File > New > Target….
  2. Um modal apresentará uma lista de possíveis destinos. role para baixo ou use o filtro para selecionar Extensão de serviço de notificação. Clique em Next.
  3. Adicione um nome de produto (use "ImageNotification" para acompanhar este tutorial), defina o idioma como Objective-C e clique em Finish.
  4. Clique para Ativar o esquema.

Etapa 2: adicionar destino ao Podfile

Adicione o pod Firebase/Messaging ao Podfile para verificar se a nova extensão tem acesso a ele:

  1. No navegador, abra o Podfile: Pods > Podfile

  2. Role até a parte inferior do arquivo e adicione:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Instale ou atualize seus pods usando pod install do diretório ios ou macos.

Etapa 3: usar o assistente de extensão

Neste momento, tudo deve estar funcionando normalmente. A etapa final é invocar o auxiliar de extensão.

  1. No navegador, selecione a extensão ImageNotification

  2. Abra o arquivo NotificationService.m.

  3. Na parte de cima do arquivo, importe FirebaseMessaging.h logo após NotificationService.h, conforme mostrado abaixo.

    Substitua o conteúdo de NotificationService.m por:

    #import "NotificationService.h"
    #import "FirebaseMessaging.h"
    #import "FirebaseAuth.h" // Add this line if you are using FirebaseAuth phone authentication
    #import <UIKit/UIKit.h> // Add this line if you are using FirebaseAuth phone authentication
    
    @interface NotificationService ()
    
    @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
    @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
    
    @end
    
    @implementation NotificationService
    
    /* Uncomment this if you are using Firebase Auth
    - (BOOL)application:(UIApplication *)app
                openURL:(NSURL *)url
                options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
      if ([[FIRAuth auth] canHandleURL:url]) {
        return YES;
      }
      return NO;
    }
    
    - (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
      for (UIOpenURLContext *urlContext in URLContexts) {
        [FIRAuth.auth canHandleURL:urlContext.URL];
      }
    }
    */
    
    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
    
        // Modify the notification content here...
        [[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler];
    }
    
    - (void)serviceExtensionTimeWillExpire {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        self.contentHandler(self.bestAttemptContent);
    }
    
    @end
    

Etapa 4: adicionar a imagem ao payload

Agora é possível adicionar uma imagem no seu payload de notificação. Consulte a documentação do iOS sobre como criar uma solicitação de envio. Um tamanho máximo de imagem de 300 KB é aplicado pelo dispositivo.