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:
- No debe ser una función anónima.
- Debe ser una función de nivel superior (p. ej., no es un método de clase que requiera la inicialización).
- 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á unFuture
que contiene unRemoteMessage
. Una vez que se consuma, se quitará elRemoteMessage
.onMessageOpenedApp
: Es unStream
que publica unRemoteMessage
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
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>
Especifica los mensajes traducidos en el directorio
values-language
. Por ejemplo, especifica los mensajes en francés enresources/values-fr/strings.xml
:<string name="notification_title">Bonjour le monde</string> <string name="notification_message">C'est un message</string>
En la carga útil del servidor, en lugar de usar las claves
title
,message
ybody
, usatitle_loc_key
ybody_loc_key
para tu mensaje localizado y configúralas en el atributoname
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
Especifica tus mensajes en idioma predeterminado en
Base.lproj/Localizable.strings
:"NOTIFICATION_TITLE" = "Hello World"; "NOTIFICATION_MESSAGE" = "This is a message";
Especifica los mensajes traducidos en el directorio
language.lproj
. Por ejemplo, especifica los mensajes en francés enfr.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
- En Xcode, haz clic en File > New > Target…
- 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.
- Agrega un nombre de producto (usa “ImageNotification” para continuar con este instructivo), establece el lenguaje en Objective-C y haz clic en Finalizar.
- 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:
Desde Navigator, abre el Podfile: Pods > Podfile
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
Instala o actualiza tus Pods con
pod install
desde el directorioios
omacos
.
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.
En el navegador, selecciona la extensión de ImageNotification.
Abre el archivo
NotificationService.m
.En la parte superior del archivo, importa
FirebaseMessaging.h
justo después deNotificationService.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.