Recibir mensajes en una aplicación Flutter

Dependiendo del estado de un dispositivo, los mensajes entrantes se manejan de manera diferente. Para comprender estos escenarios y cómo integrar FCM en su propia aplicación, primero es importante establecer los distintos estados en los que puede encontrarse un dispositivo:

Estado Descripción
Primer plano Cuando la aplicación está abierta, a la vista y en uso.
Fondo Cuando la aplicación está abierta, pero en segundo plano (minimizada). Esto suele ocurrir cuando el usuario presiona el botón "Inicio" en el dispositivo, cambia a otra aplicación usando el selector de aplicaciones o tiene la aplicación abierta en una pestaña diferente (web).
Terminado Cuando el dispositivo está bloqueado o la aplicación no se está ejecutando.

Hay algunas condiciones previas que deben cumplirse antes de que la aplicación pueda recibir cargas de mensajes a través de FCM:

  • La solicitud debe haberse abierto al menos una vez (para permitir el registro en FCM).
  • En iOS, si el usuario elimina la aplicación del selector de aplicaciones, debe volver a abrirla manualmente para que los mensajes en segundo plano comiencen a funcionar nuevamente.
  • En Android, si el usuario fuerza el cierre de la aplicación desde la configuración del dispositivo, debe volver a abrirla manualmente para que los mensajes comiencen a funcionar.
  • En la web, debe haber solicitado un token (usando getToken() ) con su certificado push web.

Solicitar permiso para recibir mensajes

En iOS, macOS, web y Android 13 (o posterior), antes de poder recibir cargas útiles de FCM en su dispositivo, primero debe solicitar permiso al usuario.

El paquete firebase_messaging proporciona una API sencilla para solicitar permiso mediante el método requestPermission . Esta API acepta una serie de argumentos con nombre que definen el tipo de permisos que le gustaría solicitar, como por ejemplo si los mensajes que contienen cargas útiles de notificación pueden activar un sonido o leer mensajes a través de Siri. De forma predeterminada, el método solicita permisos predeterminados sensatos. La API de referencia proporciona documentación completa sobre para qué sirve cada permiso.

Para comenzar, llame al método desde su aplicación (en iOS se mostrará un modal nativo, en la web se activará el flujo API nativo del navegador):

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}');

La propiedad authorizationStatus del objeto NotificationSettings devuelto por la solicitud se puede utilizar para determinar la decisión general del usuario:

  • authorized : El usuario otorgó permiso.
  • denied : el usuario negó el permiso.
  • notDetermined : el usuario aún no ha elegido si otorgar permiso.
  • provisional : El usuario otorgó permiso provisional

Las otras propiedades en NotificationSettings indican si un permiso específico está habilitado, deshabilitado o no es compatible con el dispositivo actual.

Una vez que se haya otorgado el permiso y se hayan comprendido los diferentes tipos de estado del dispositivo, su aplicación ahora puede comenzar a manejar las cargas útiles FCM entrantes.

Manejo de mensajes

Según el estado actual de su aplicación, las cargas útiles entrantes de diferentes tipos de mensajes requieren diferentes implementaciones para manejarlas:

Mensajes en primer plano

Para manejar mensajes mientras su aplicación está en primer plano, escuche la transmisión 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}');
  }
});

La transmisión contiene un RemoteMessage que detalla diversa información sobre la carga útil, como de dónde era, el ID único, la hora de envío, si contenía una notificación y más. Dado que el mensaje se recuperó mientras su aplicación estaba en primer plano, puede acceder directamente al estado y contexto de su aplicación Flutter.

Mensajes de primer plano y de notificación

Los mensajes de notificación que llegan mientras la aplicación está en primer plano no mostrarán una notificación visible de forma predeterminada, tanto en Android como en iOS. Sin embargo, es posible anular este comportamiento:

  • En Android, debes crear un canal de notificación de "alta prioridad".
  • En iOS, puedes actualizar las opciones de presentación de la aplicación.

Mensajes de fondo

El proceso de manejo de mensajes en segundo plano es diferente en plataformas nativas (Android y Apple) y basadas en web.

Plataformas Apple y Android

Maneje mensajes en segundo plano registrando un controlador onBackgroundMessage . Cuando se reciben mensajes, se genera un aislamiento (solo Android, iOS/macOS no requiere un aislamiento separado) que le permite manejar mensajes incluso cuando su aplicación no se está ejecutando.

Hay algunas cosas que debes tener en cuenta sobre tu controlador de mensajes en segundo plano:

  1. No debe ser una función anónima.
  2. Debe ser una función de nivel superior (por ejemplo, no un método de clase que requiera inicialización).
  3. Cuando se usa Flutter versión 3.3.0 o superior, el controlador de mensajes debe anotarse con @pragma('vm:entry-point') justo encima de la declaración de función (de lo contrario, se puede eliminar durante la agitación del árbol para el modo de lanzamiento).
@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());
}

Dado que el controlador se ejecuta de forma aislada fuera del contexto de su aplicación, no es posible actualizar el estado de la aplicación ni ejecutar ninguna lógica que afecte la interfaz de usuario. Sin embargo, puede realizar lógica como solicitudes HTTP, realizar operaciones de IO (por ejemplo, actualizar el almacenamiento local), comunicarse con otros complementos, etc.

También se recomienda completar su lógica lo antes posible. La ejecución de tareas largas e intensivas afecta el rendimiento del dispositivo y puede hacer que el sistema operativo finalice el proceso. Si las tareas se ejecutan durante más de 30 segundos, el dispositivo puede finalizar automáticamente el proceso.

Web

En la Web, escriba un trabajador de servicio JavaScript que se ejecute en segundo plano. Utilice el trabajador de servicio para manejar mensajes en segundo plano.

Para comenzar, cree un nuevo archivo en su directorio web y llámelo 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);
});

El archivo debe importar tanto la aplicación como los SDK de mensajería, inicializar Firebase y exponer la variable messaging .

A continuación, se debe dar de alta al trabajador. Dentro del archivo de entrada, después de que se haya cargado el archivo main.dart.js , registre a su trabajador:

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

Luego reinicie su aplicación Flutter. El trabajador quedará registrado y cualquier mensaje de fondo se manejará a través de este archivo.

Manejo de la interacción

Dado que las notificaciones son una señal visible, es común que los usuarios interactúen con ellas (presionando). El comportamiento predeterminado tanto en Android como en iOS es abrir la aplicación. Si se da por terminada la solicitud se iniciará; si está en segundo plano, pasará a primer plano.

Dependiendo del contenido de una notificación, es posible que desee controlar la interacción del usuario cuando se abre la aplicación. Por ejemplo, si se envía un nuevo mensaje de chat a través de una notificación y el usuario lo presiona, es posible que desee abrir la conversación específica cuando se abra la aplicación.

El paquete firebase-messaging proporciona dos formas de manejar esta interacción:

  • getInitialMessage() : si la aplicación se abre desde un estado terminado, se devolverá un Future que contiene un RemoteMessage . Una vez consumido, el RemoteMessage se eliminará.
  • onMessageOpenedApp : una Stream que publica un RemoteMessage cuando la aplicación se abre desde un estado en segundo plano.

Se recomienda manejar ambos escenarios para garantizar una experiencia de usuario fluida para sus usuarios. El siguiente ejemplo de código describe cómo se puede lograr esto:

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("...");
  }
}

La forma en que maneja la interacción depende de la configuración de su aplicación. El ejemplo anterior muestra una ilustración básica utilizando un StatefulWidget.

Localizar mensajes

Puede enviar cadenas localizadas de dos maneras diferentes:

  • Almacena el idioma preferido de cada uno de tus usuarios en tu servidor y envía notificaciones personalizadas para cada idioma
  • Incruste cadenas localizadas en su aplicación y utilice la configuración regional nativa del sistema operativo.

A continuación se explica cómo utilizar el segundo método:

Androide

  1. Especifique sus mensajes en el idioma predeterminado en resources/values/strings.xml :

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Especifique los mensajes traducidos en el directorio values- language . Por ejemplo, especifique mensajes en francés en resources/values-fr/strings.xml :

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. En la carga útil del servidor, en lugar de usar las claves title , message y body , use title_loc_key y body_loc_key para su mensaje localizado y configúrelos en el atributo de name del mensaje que desea mostrar.

    La carga útil del mensaje se vería así:

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

iOS

  1. Especifique sus mensajes en el idioma predeterminado en Base.lproj/Localizable.strings :

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Especifique los mensajes traducidos en el directorio language .lproj . Por ejemplo, especifique mensajes en francés en fr.lproj/Localizable.strings :

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

    La carga útil del mensaje se vería así:

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

Habilitar la exportación de datos de entrega de mensajes

Puede exportar los datos de su mensaje a BigQuery para su posterior análisis. BigQuery le permite analizar los datos usando BigQuery SQL, exportarlos a otro proveedor de nube o usar los datos para sus modelos de ML personalizados. Una exportación a BigQuery incluye todos los datos disponibles para los mensajes, independientemente del tipo de mensaje o de si el mensaje se envía a través de la API o del redactor de notificaciones.

Para habilitar la exportación, primero siga los pasos que se describen aquí y luego siga estas instrucciones:

Androide

Puedes utilizar el siguiente código:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

Para iOS, debe cambiar AppDelegate.m con el siguiente contenido.

#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, debe cambiar su trabajador de servicio para poder utilizar la versión v9 del SDK. La versión v9 debe estar empaquetada, por lo que debe usar un paquete como esbuild por ejemplo, para que el trabajador del servicio funcione. Consulte la aplicación de ejemplo para ver cómo lograrlo.

Una vez que haya migrado al SDK v9, puede usar el siguiente código:

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

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

No olvide ejecutar yarn build para exportar la nueva versión de su trabajador de servicio a la carpeta web .

Mostrar imágenes en notificaciones en iOS

En los dispositivos Apple, para que las notificaciones FCM entrantes muestren imágenes de la carga útil de FCM, debe agregar una extensión de servicio de notificación adicional y configurar su aplicación para usarla.

Si está utilizando la autenticación telefónica de Firebase, debe agregar el pod de autenticación de Firebase a su Podfile.

Paso 1: agregar una extensión del servicio de notificaciones

  1. En Xcode, haga clic en Archivo > Nuevo > Destino...
  2. Un modal presentará una lista de posibles objetivos; desplácese hacia abajo o use el filtro para seleccionar Extensión del servicio de notificación . Haga clic en Siguiente .
  3. Agregue un nombre de producto (use "ImageNotification" para seguir este tutorial), configure el idioma en Objective-C y haga clic en Finalizar .
  4. Habilite el esquema haciendo clic en Activar .

Paso 2: agregar destino al Podfile

Asegúrese de que su nueva extensión tenga acceso al pod Firebase/Messaging agregándola en el Podfile:

  1. Desde el Navegador, abra el Podfile: Pods > Podfile

  2. Desplácese hasta el final del archivo y agregue:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Instale o actualice sus pods usando pod install desde el directorio ios o macos .

Paso 3: utiliza la extensión auxiliar

En este punto, todo debería seguir funcionando con normalidad. El último paso es invocar la extensión auxiliar.

  1. Desde el navegador, seleccione su extensión ImageNotification

  2. Abra el archivo NotificationService.m .

  3. En la parte superior del archivo, importe FirebaseMessaging.h justo después de NotificationService.h como se muestra a continuación.

    Reemplace el contenido de NotificationService.m con:

    #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
    

Paso 4: agrega la imagen a la carga útil

En su carga útil de notificaciones, ahora puede agregar una imagen. Consulte la documentación de iOS sobre cómo crear una solicitud de envío . Tenga en cuenta que el dispositivo exige un tamaño de imagen máximo de 300 KB.