Receba mensagens em um aplicativo Flutter

Dependendo do estado do dispositivo, as mensagens recebidas são tratadas de forma diferente. Para entender esses cenários e como integrar o FCM em sua própria aplicação, primeiro é importante estabelecer os vários estados em que um dispositivo pode estar:

Estado Descrição
Primeiro plano Quando o aplicativo está aberto, visualizado e em uso.
Fundo Quando o aplicativo está aberto, mas em segundo plano (minimizado). Isso normalmente ocorre quando o usuário pressionou o botão "home" no dispositivo, mudou para outro aplicativo usando o alternador de aplicativos ou abriu o aplicativo em uma guia diferente (web).
Terminado Quando o dispositivo está bloqueado ou o aplicativo não está em execução.

Existem alguns pré-requisitos que devem ser atendidos antes que o aplicativo possa receber cargas de mensagens via FCM:

  • A aplicação deve ter sido aberta pelo menos uma vez (para permitir o registo na FCM).
  • No iOS, se o usuário deslizar o aplicativo para fora do alternador de aplicativos, ele deverá 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 aplicativo nas configurações do dispositivo, ele deverá ser reaberto manualmente para que as mensagens comecem a funcionar.
  • Na web, você deve ter solicitado um token (usando getToken() ) com seu certificado web push.

Solicitar permissão para receber mensagens

No iOS, macOS, web e Android 13 (ou mais recente), antes que as cargas do FCM possam ser recebidas no seu dispositivo, você deve primeiro pedir permissão ao usuário.

O pacote firebase_messaging fornece uma API simples para solicitar permissão por meio do método requestPermission . Esta API aceita vários argumentos nomeados que definem o tipo de permissões que você gostaria de solicitar, como se mensagens contendo cargas de notificação podem acionar um som ou ler mensagens via Siri. Por padrão, o método solicita permissões padrão sensatas. A API de referência fornece documentação completa sobre a finalidade de cada permissão.

Para começar, chame o método da sua aplicação (no iOS será exibido um modal nativo, na web o fluxo da API nativa 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 authorizationStatus do objeto NotificationSettings retornado 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 permissão.
  • notDetermined : o usuário ainda não escolheu se deseja conceder permissão.
  • provisional : o usuário concedeu permissão provisória

As outras propriedades em NotificationSettings retornam se uma permissão específica está habilitada, desabilitada ou não tem suporte no 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 lidar com as cargas úteis do FCM recebidas.

Tratamento de mensagens

Com base no estado atual do seu aplicativo, cargas recebidas de diferentes tipos de mensagens exigem implementações diferentes para lidar com elas:

Mensagens em primeiro plano

Para lidar com 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 fluxo contém um RemoteMessage , detalhando diversas informações sobre a carga útil, como de onde ela veio, o ID exclusivo, a hora de envio, se continha uma notificação e muito mais. Como a mensagem foi recuperada enquanto seu aplicativo estava em primeiro plano, você pode acessar diretamente o estado e o contexto do seu aplicativo Flutter.

Mensagens de primeiro plano e de notificação

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

  • No Android, você deve 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 tratamento de mensagens em segundo plano é diferente em plataformas nativas (Android e Apple) e baseadas na web.

Plataformas Apple e Android

Lide com mensagens em segundo plano registrando um manipulador onBackgroundMessage . Quando as mensagens são recebidas, um isolamento é gerado (somente Android, iOS/macOS não requer um isolamento separado) permitindo que você manipule mensagens mesmo quando seu aplicativo não estiver em execução.

Há algumas coisas que você deve ter em mente sobre o gerenciador de mensagens em segundo plano:

  1. Não deve ser uma função anônima.
  2. Deve 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 versão 3.3.0 ou superior, o manipulador de mensagens deve ser anotado com @pragma('vm:entry-point') logo acima da declaração da função (caso contrário, ele pode ser removido durante a agitação da árvore para o modo de liberação).
@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 manipulador é executado isoladamente, fora do contexto do aplicativo, não é possível atualizar o estado do aplicativo ou executar qualquer lógica que afete a interface do usuário. Você pode, no entanto, executar lógicas como solicitações HTTP, realizar operações IO (por exemplo, atualizar o armazenamento local), comunicar-se com outros plug-ins, etc.

Também é recomendado completar sua lógica o mais rápido possível. A execução de tarefas longas e intensas afeta o desempenho do dispositivo e pode fazer com que o sistema operacional encerre o processo. Se as tarefas forem executadas por mais de 30 segundos, o dispositivo poderá encerrar automaticamente o processo.

Rede

Na Web, escreva um Service Worker JavaScript que seja executado em segundo plano. Use o service worker para lidar com mensagens em segundo plano.

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

importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js");

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

const messaging = firebase.messaging();

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

O arquivo deve importar os SDKs do aplicativo e de mensagens, inicializar o Firebase e expor a variável messaging .

Em seguida, o trabalhador deve ser cadastrado. Dentro do arquivo de entrada, após o carregamento do arquivo main.dart.js , registre seu trabalhador:

<html>
<body>
  ...
  <script src="main.dart.js" type="application/javascript"></script>
  <script>
       if ('serviceWorker' in navigator) {
          // Service workers are supported. Use them.
          window.addEventListener('load', function () {
            // ADD THIS LINE
            navigator.serviceWorker.register('/firebase-messaging-sw.js');

            // 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;

            //  ...
          });
      }
  </script>

Em seguida, reinicie seu aplicativo Flutter. O trabalhador será registrado e quaisquer mensagens em segundo plano serão tratadas por meio deste arquivo.

Lidando com interação

Como as notificações são uma dica 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 será iniciado; se estiver em segundo plano, será colocado em primeiro plano.

Dependendo do conteúdo de uma notificação, você pode querer lidar com a interação do usuário quando o aplicativo for aberto. Por exemplo, se uma nova mensagem de bate-papo for enviada por meio de uma notificação e o usuário pressioná-la, você pode querer abrir a conversa específica quando o aplicativo for aberto.

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

  • getInitialMessage() : Se o aplicativo for aberto a partir de um estado finalizado, um Future contendo uma RemoteMessage será retornado. Uma vez consumido, o RemoteMessage será removido.
  • onMessageOpenedApp : um Stream que publica um RemoteMessage quando o aplicativo é aberto em um estado de segundo plano.

É recomendável que ambos os cenários sejam tratados para garantir uma experiência de usuário tranquila para seus usuários. O exemplo de código abaixo descreve como isso pode ser alcançado:

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 maneira como você lida com a interação depende da configuração do seu aplicativo. O exemplo acima mostra uma ilustração básica usando um StatefulWidget.

Localizar mensagens

Você pode enviar strings localizadas de duas maneiras diferentes:

  • Armazene o idioma preferido de cada um de seus usuários em seu servidor e envie notificações personalizadas para cada idioma
  • Incorpore strings localizadas em seu aplicativo e use as configurações de localidade nativas do sistema operacional

Veja como usar o segundo método:

Android

  1. Especifique suas 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 . Por exemplo, especifique 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. Na carga útil do servidor, em vez de usar as chaves title , message e body , use title_loc_key e body_loc_key para sua mensagem localizada e defina-as como o atributo name da mensagem que você deseja exibir.

    A carga útil da mensagem ficaria assim:

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

iOS

  1. Especifique suas mensagens de 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 . Por exemplo, especifique mensagens em francês em fr.lproj/Localizable.strings :

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

    A carga útil da mensagem ficaria assim:

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

Habilitar exportação de dados de entrega de mensagens

Você pode exportar os dados da sua mensagem para o BigQuery para análise posterior. O BigQuery permite analisar os dados usando o BigQuery SQL, exportá-los para outro provedor de nuvem ou usar os dados para seus modelos de ML personalizados. Uma exportação para o BigQuery inclui todos os dados disponíveis para mensagens, independentemente do tipo de mensagem ou se a mensagem é enviada pela API ou pelo compositor do Notifications.

Para ativar a exportação, primeiro siga as etapas descritas aqui e depois siga estas instruções:

Android

Você pode usar o seguinte código:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

Para iOS, você precisa alterar o AppDelegate.m com o seguinte conteúdo.

#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

Rede

Para Web, você precisa alterar seu service worker para usar a versão v9 do SDK. A versão v9 precisa ser empacotada, então você precisa usar um empacotador como esbuild por exemplo, para fazer o service worker funcionar. Veja o aplicativo de exemplo para ver como conseguir isso.

Depois de migrar para o SDK v9, você poderá usar o seguinte código:

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

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

Não se esqueça de executar yarn build para exportar a nova versão do seu service worker para a pasta web .

Exibir imagens em notificações no iOS

Em dispositivos Apple, para que as notificações FCM recebidas exibam imagens da carga útil do FCM, você deve adicionar uma extensão de serviço de notificação adicional e configurar seu aplicativo para usá-la.

Se você estiver usando a autenticação por telefone do Firebase, deverá adicionar 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 Arquivo > Novo > Destino...
  2. Um modal apresentará uma lista de possíveis alvos; role para baixo ou use o filtro para selecionar Extensão do serviço de notificação . Clique em Avançar .
  3. Adicione um nome de produto (use "ImageNotification" para acompanhar este tutorial), defina o idioma como Objective-C e clique em Finish .
  4. Habilite o esquema clicando em Ativar .

Passo 2 – Adicionar destino ao Podfile

Certifique-se de que sua nova extensão tenha acesso ao pod Firebase/Messaging adicionando-a ao Podfile:

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

  2. Role para baixo até o final 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 no diretório ios ou macos .

Passo 3 – Use o auxiliar de extensão

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

  1. No navegador, selecione sua extensão ImageNotification

  2. Abra o arquivo NotificationService.m .

  3. Na parte superior 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
    

Passo 4 – Adicione a imagem à carga útil

Na sua carga de notificação, agora você pode adicionar uma imagem. Consulte a documentação do iOS sobre como criar uma solicitação de envio . Lembre-se de que o tamanho máximo de imagem de 300 KB é imposto pelo dispositivo.