Recibe mensajes en una app de Flutter

Según el estado del dispositivo, los mensajes entrantes se manejan de manera diferente. Para comprender estas situaciones y aprender a integrar FCM en tu propia aplicación, primero es importante establecer los diversos estados en los que puede estar un dispositivo:

Estado Descripción
Primer plano Cuando la aplicación está abierta, es visible y se usa.
Segundo plano Cuando la aplicación está abierta, pero en segundo plano (minimizada). Esto suele ocurrir cuando el usuario presiona el botón de inicio del dispositivo, se cambia a otra app con el selector de apps o la aplicación se abre en una pestaña diferente (en la Web).
Finalizada Cuando el dispositivo está bloqueado o no se está ejecutando la aplicación.

Para que la aplicación pueda recibir cargas útiles de mensajes a través de FCM, se deben cumplir las condiciones previas que se indican a continuación:

  • Se debe abrir la aplicación al menos una vez (para permitir el registro en FCM).
  • En iOS, si el usuario desliza el dedo a fin de descartar la aplicación desde el selector de apps, debes volver a abrirla manualmente para que los mensajes en segundo plano comiencen a funcionar de nuevo.
  • En Android, si el usuario fuerza el cierre de la app desde la configuración del dispositivo, esta se debe volver a abrir manualmente para que los mensajes comiencen a funcionar.
  • En la Web, debes haber solicitado un token (con getToken()) con tu certificado push web.

Solicita permiso para recibir mensajes

En iOS, macOS, la Web y Android 13 (o versiones posteriores), antes de recibir cargas útiles de FCM en tu dispositivo, debes pedir el permiso del usuario.

El paquete firebase_messaging proporciona una API simple para solicitar permisos mediante el método requestPermission. Esta API acepta varios argumentos con nombre que definen el tipo de permisos que te gustaría solicitar, 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 razonables. La API de referencia proporciona documentación completa sobre para qué se usa cada permiso.

Para comenzar, llama al método desde tu aplicación (en iOS, se mostrará una ventana modal nativa; en la Web, se activará el flujo de API nativa 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 que se muestra en la solicitud se puede usar para determinar la decisión general del usuario:

  • authorized: El usuario otorgó permiso.
  • denied: El usuario rechazó el permiso.
  • notDetermined: El usuario aún no eligió otorgar permiso.
  • provisional: Es el usuario al que se le otorgó permiso provisional.

Las otras propiedades de NotificationSettings muestran si un permiso específico está habilitado, inhabilitado 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, tu aplicación podrá comenzar a controlar las cargas útiles entrantes de FCM.

Controla mensajes

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

Mensajes en primer plano

Para procesar los mensajes mientras la aplicación está en primer plano, detecta 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 con información detallada sobre la carga útil, como de dónde proviene, el ID único, la hora de envío, si se incluyó una notificación y mucho más. Como el mensaje se recuperó mientras la aplicación se encontraba en primer plano, puedes acceder directamente al estado y al contexto de tu aplicación de Flutter.

Mensajes en primer plano y notificaciones

Los mensajes de notificación que lleguen 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 de las siguientes maneras:

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

Mensajes en segundo plano

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

Plataformas de Apple y Android

Para manejar mensajes en segundo plano, registra un controlador onBackgroundMessage. Cuando se reciben mensajes, se genera un aislamiento (solo para Android, ya que iOS y macOS no requieren un aislamiento independiente), lo que te permite controlar mensajes incluso cuando la aplicación no está en ejecución.

Con respecto a tu controlador de mensajes en segundo plano, debes tener en cuenta los siguientes aspectos:

  1. No debe ser una función anónima.
  2. Debe ser una función de nivel superior (p. ej., no es un método de clase que requiera la inicialización).
  3. Cuando usas Flutter 3.3.0 o una versión posterior, el controlador de mensajes debe tener la anotación @pragma('vm:entry-point') justo encima de la declaración de la función (de lo contrario, se puede quitar durante la eliminación de código no utilizado 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 en su propio aislamiento fuera del contexto de tus aplicaciones, no es posible actualizar el estado de la aplicación ni ejecutar ninguna lógica que afecte a la IU. Sin embargo, puedes realizar una lógica, como solicitudes HTTP, realizar operaciones de E/S (p. ej., actualizar el almacenamiento local), comunicarte con otros complementos, etcétera.

También te recomendamos que completes la lógica lo antes posible. Ejecutar tareas intensivas y prolongadas afecta el rendimiento del dispositivo y puede hacer que el SO cancele 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, escribe un service worker de JavaScript que se ejecute en segundo plano. Úsalo a fin de controlar los mensajes en segundo plano.

Para comenzar, crea un archivo nuevo en el directorio web y asígnale el nombre 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);
});

El archivo debe importar los SDK de la app y los mensajes, inicializar Firebase y exponer la variable messaging.

A continuación, el trabajador debe estar registrado. Dentro del archivo index.html, registra el trabajador; para ello, modifica la etiqueta <script> que inicia 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>

Si aún usas el sistema de plantillas anterior, puedes registrar el trabajador; para ello, modifica la etiqueta <script> que inicia Flutter de la siguiente manera:

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

A continuación, reinicia tu aplicación de Flutter. Se registrará el trabajador y los mensajes en segundo plano se manejarán mediante este archivo.

Controla las interacciones

Dado que las notificaciones son una indicación visible, es común que los usuarios interactúen con ellas (presionándolas). El comportamiento predeterminado en dispositivos iOS y Android es abrir la aplicación. Si la aplicación está cerrada, se iniciará y si está en segundo plano, pasará a primer plano.

Según el contenido de la notificación, es posible que quieras controlar la interacción del usuario cuando se abra la aplicación. Por ejemplo, si la notificación contiene un nuevo mensaje de chat y el usuario la presiona, es posible que quieras que la aplicación se abra en la conversación específica.

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

  • getInitialMessage(): Si la aplicación se abre y antes estaba cerrada, se mostrará un Future que contiene un RemoteMessage. Una vez que se consuma, se quitará el RemoteMessage.
  • onMessageOpenedApp: Es un Stream que publica un RemoteMessage cuando la aplicación se abre desde un estado en segundo plano.

Se recomienda que ambas situaciones se controlen a fin de garantizar una UX fluida. En el siguiente ejemplo de código, se describe cómo hacerlo:

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 controlas las interacciones depende de la configuración de tu app. En el ejemplo anterior, se muestra una ilustración básica con un StatefulWidget.

Localiza mensajes

Puedes enviar strings localizadas de las siguientes maneras:

  • Almacena el idioma preferido de cada uno de tus usuarios en tu servidor y envía notificaciones personalizadas para cada idioma.
  • Incorpora strings localizadas en tu app y usa la configuración regional nativa del sistema operativo.

Aquí te indicamos cómo usar el segundo método:

Android

  1. Especifica tus mensajes en idioma predeterminado en resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Especifica los mensajes traducidos en el directorio values-language. Por ejemplo, especifica los 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, usa title_loc_key y body_loc_key para tu mensaje localizado y configúralas en el atributo name del mensaje que deseas mostrar.

    La carga útil del mensaje se vería de la siguiente manera:

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

iOS

  1. Especifica tus mensajes en idioma predeterminado en Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Especifica los mensajes traducidos en el directorio language.lproj. Por ejemplo, especifica los 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 de la siguiente manera:

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

Habilita la exportación de datos de entrega de mensajes

Puedes exportar tus datos de mensajes a BigQuery para analizarlos en detalle. Esta herramienta te permite analizar los datos con BigQuery SQL, exportarlos a otro proveedor de servicios en la nube o usarlos en tus modelos de AA personalizados. La exportación a BigQuery incluye todos los datos disponibles para los mensajes, independientemente del tipo de mensaje o si este se envía a través de la API o del Compositor de Notifications.

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

Android

Puedes usar el siguiente código:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

Para iOS, debes 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 la Web, debes cambiar tu service worker a fin de usar la versión 9 del SDK. La versión 9 debe empaquetarse, por lo que debes usar un agrupador como esbuild para que funcione el service worker. Consulta la app de ejemplo para ver cómo lograrlo.

Una vez que hayas migrado al SDK de la versión 9, puedes usar el siguiente código:

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

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

No olvides ejecutar yarn build para exportar la nueva versión del service worker a la carpeta web.

Muestra imágenes en las notificaciones de iOS

Para que las notificaciones de FCM entrantes muestren imágenes de la carga útil de FCM en los dispositivos Apple, debes agregar una extensión de servicio de notificación adicional y configurar tu app para usarla.

Si usas la autenticación por teléfono de Firebase, debes agregar el Pod de Firebase Auth a tu Podfile.

Paso 1: Agrega una extensión de servicio de notificación

  1. En Xcode, haz clic en File > New > Target…
  2. Una opción modal presentará una lista de destinos posibles. Desplázate hacia abajo o utiliza el filtro para seleccionar la Extensión del servicio de notificaciones. Haz clic en Siguiente.
  3. Agrega un nombre de producto (usa “ImageNotification” para continuar con este instructivo), establece el lenguaje en Objective-C y haz clic en Finalizar.
  4. Para habilitar el esquema, haz clic en Activar.

Paso 2: Agrega un destino al Podfile

Para asegurarte de que la extensión nueva tenga acceso al Pod Firebase/Messaging, agrégala en el Podfile:

  1. Desde Navigator, abre el Podfile: Pods > Podfile

  2. Desplázate hasta el final del archivo y agrega lo siguiente:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Instala o actualiza tus Pods con pod install desde el directorio ios o macos.

Paso 3: Usa el asistente de extensión

En este punto, todo debería funcionar con normalidad. El paso final es invocar al auxiliar de extensiones.

  1. En el navegador, selecciona la extensión de ImageNotification.

  2. Abre el archivo NotificationService.m.

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

    Reemplaza el contenido de NotificationService.m con lo siguiente:

    #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 tu carga útil de notificaciones, ahora puedes agregar una imagen. Consulta la documentación de iOS sobre cómo crear una solicitud de envío. Ten en cuenta que el dispositivo aplica un tamaño de imagen máximo de 300 KB.