Recevoir des messages dans une application Flutter

Selon l'état de l'appareil, les messages entrants sont gérés différemment. Pour comprendre ces scénarios et savoir comment intégrer FCM à votre propre application, il est d'abord important d'établir les différents états dans lesquels un appareil peut se trouver:

État Description
Premier plan Lorsque l'application est ouverte, visible et utilisée.
Contexte Lorsque l'application est ouverte, mais en arrière-plan (minimisée). Cela se produit généralement lorsque l'utilisateur a appuyé sur le bouton "Accueil" de l'appareil, a changé d'application à l'aide du sélecteur d'applications ou a ouvert l'application dans un autre onglet (Web).
Résilié Lorsque l'appareil est verrouillé ou que l'application n'est pas en cours d'exécution

Avant que l'application puisse recevoir des charges utiles de message via FCM, plusieurs conditions préalables doivent être remplies:

  • L'application doit avoir été ouverte au moins une fois (pour permettre l'enregistrement auprès de FCM).
  • Sur iOS, si l'utilisateur balaie l'application depuis le sélecteur d'applications, il doit la rouvrir manuellement pour que les messages en arrière-plan recommencent à fonctionner.
  • Sur Android, si l'utilisateur ferme de force l'application dans les paramètres de l'appareil, il doit la rouvrir manuellement pour que les messages fonctionnent.
  • Sur le Web, vous devez avoir demandé un jeton (à l'aide de getToken()) avec votre certificat de push Web.

Demander l'autorisation de recevoir des messages

Sur iOS, macOS, le Web et Android 13 (ou version ultérieure), vous devez d'abord demander l'autorisation de l'utilisateur avant que les charges utiles FCM puissent être reçues sur votre appareil.

Le package firebase_messaging fournit une API simple pour demander une autorisation via la méthode requestPermission. Cette API accepte un certain nombre d'arguments nommés qui définissent le type d'autorisations que vous souhaitez demander, par exemple si les messages contenant des charges utiles de notification peuvent déclencher un son ou lire les messages via Siri. Par défaut, la méthode demande des autorisations par défaut sensibles. L'API de référence fournit une documentation complète sur l'utilité de chaque autorisation.

Pour commencer, appelez la méthode à partir de votre application (sur iOS, un modal natif s'affiche, sur le Web, le flux d'API natif du navigateur est déclenché):

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 propriété authorizationStatus de l'objet NotificationSettings renvoyé par la requête peut être utilisée pour déterminer la décision globale de l'utilisateur:

  • authorized: l'utilisateur a accordé l'autorisation.
  • denied: l'utilisateur a refusé l'autorisation.
  • notDetermined: l'utilisateur n'a pas encore choisi d'accorder l'autorisation.
  • provisional: l'utilisateur a accordé une autorisation provisoire

Les autres propriétés de NotificationSettings indiquent si une autorisation spécifique est activée, désactivée ou non prise en charge sur l'appareil actuel.

Une fois l'autorisation accordée et les différents types d'état de l'appareil compris, votre application peut commencer à gérer les charges utiles FCM entrantes.

Traitement des messages

En fonction de l'état actuel de votre application, les charges utiles entrantes de différents types de messages nécessitent différentes implémentations pour les gérer:

Messages de premier plan

Pour gérer les messages lorsque votre application est au premier plan, écoutez le flux 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}');
  }
});

Le flux contient un RemoteMessage, qui détaille diverses informations sur la charge utile, telles que son origine, son ID unique, l'heure d'envoi, si elle contenait une notification, etc. Étant donné que le message a été récupéré lorsque votre application est au premier plan, vous pouvez accéder directement à l'état et au contexte de votre application Flutter.

Messages de premier plan et de notification

Les messages de notification qui arrivent lorsque l'application est exécutée au premier plan n'affichent pas de notification visible par défaut, à la fois sur Android et iOS. Il est toutefois possible de remplacer ce comportement:

  • Sur Android, vous devez créer un canal de notification "Haute priorité".
  • Sur iOS, vous pouvez modifier les options de présentation de l'application.

Messages en arrière-plan

La gestion des messages en arrière-plan est différente sur les plates-formes natives (Android et Apple) et Web.

Plates-formes Apple et Android

Gérez les messages en arrière-plan en enregistrant un gestionnaire onBackgroundMessage. Lorsqu'un message est reçu, un isolate est créé (Android uniquement, iOS/macOS ne nécessite pas d'isolate distinct) qui vous permet de gérer les messages même lorsque votre application n'est pas en cours d'exécution.

Voici quelques points à prendre en compte concernant votre gestionnaire de messages en arrière-plan:

  1. Il ne doit pas s'agir d'une fonction anonyme.
  2. Il doit s'agir d'une fonction de niveau supérieur (par exemple, pas une méthode de classe qui nécessite une initialisation).
  3. Lorsque vous utilisez Flutter 3.3.0 ou une version ultérieure, le gestionnaire de messages doit être annoté avec @pragma('vm:entry-point') juste au-dessus de la déclaration de fonction (sinon, il risque d'être supprimé lors du nettoyage de l'arborescence pour le mode de publication).
@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());
}

Étant donné que le gestionnaire s'exécute dans son propre isolate en dehors du contexte de votre application, il n'est pas possible de mettre à jour l'état de l'application ni d'exécuter une logique affectant l'UI. Vous pouvez toutefois effectuer une logique telle que des requêtes HTTP, des opérations d'E/S (par exemple, la mise à jour du stockage local), la communication avec d'autres plug-ins, etc.

Nous vous recommandons également de terminer votre logique dès que possible. L'exécution de tâches longues et intensives a un impact sur les performances de l'appareil et peut entraîner l'arrêt du processus par l'OS. Si les tâches s'exécutent pendant plus de 30 secondes, l'appareil peut arrêter automatiquement le processus.

Web

Sur le Web, écrivez un service worker JavaScript qui s'exécute en arrière-plan. Utilisez le service worker pour gérer les messages en arrière-plan.

Pour commencer, créez un fichier dans votre répertoire web et appelez-le 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);
});

Le fichier doit importer les SDK d'application et de messagerie, initialiser Firebase et exposer la variable messaging.

Ensuite, le nœud de calcul doit être enregistré. Dans le fichier index.html, enregistrez le nœud de calcul en modifiant la balise <script> qui lance 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 vous utilisez toujours l'ancien système de création de modèles, vous pouvez enregistrer le worker en modifiant la balise <script> qui démarre Flutter comme suit:

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

Redémarrez ensuite votre application Flutter. Le nœud de calcul sera enregistré et tous les messages en arrière-plan seront gérés via ce fichier.

Gérer les interactions

Étant donné que les notifications sont un indice visible, il est courant que les utilisateurs interagissent avec elles (en appuyant dessus). Le comportement par défaut sur Android et iOS consiste à ouvrir l'application. Si l'application est arrêtée, elle sera démarrée. Si elle est en arrière-plan, elle sera mise au premier plan.

En fonction du contenu d'une notification, vous pouvez gérer l'interaction de l'utilisateur lorsque l'application s'ouvre. Par exemple, si un nouveau message de chat est envoyé via une notification et que l'utilisateur appuie dessus, vous pouvez ouvrir la conversation spécifique lorsque l'application s'ouvre.

Le package firebase-messaging propose deux méthodes pour gérer cette interaction:

  • getInitialMessage(): si l'application est ouverte à partir d'un état arrêté, un Future contenant un RemoteMessage est renvoyé. Une fois consommé, le RemoteMessage est supprimé.
  • onMessageOpenedApp: Stream qui publie un RemoteMessage lorsque l'application est ouverte à partir d'un état en arrière-plan.

Nous vous recommandons de gérer les deux scénarios pour garantir une expérience utilisateur fluide. L'exemple de code ci-dessous montre comment procéder:

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 manière dont vous gérez l'interaction dépend de la configuration de votre application. L'exemple ci-dessus illustre une illustration de base à l'aide d'un StatefulWidget.

Localiser Messages

Vous pouvez envoyer des chaînes localisées de deux manières différentes:

  • Stockez la langue préférée de chacun de vos utilisateurs sur votre serveur et envoyez des notifications personnalisées pour chaque langue.
  • Intégrer des chaînes localisées dans votre application et utiliser les paramètres régionaux natifs du système d'exploitation

Pour utiliser la deuxième méthode, procédez comme suit:

Android

  1. Spécifiez vos messages dans la langue par défaut dans resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Spécifiez les messages traduits dans le répertoire values-language. Par exemple, spécifiez des messages en français dans resources/values-fr/strings.xml:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. Dans la charge utile du serveur, au lieu d'utiliser les clés title, message et body, utilisez title_loc_key et body_loc_key pour votre message localisé, et définissez-les sur l'attribut name du message que vous souhaitez afficher.

    La charge utile du message se présente comme suit:

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

iOS

  1. Spécifiez vos messages dans la langue par défaut dans Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Spécifiez les messages traduits dans le répertoire language.lproj. Par exemple, spécifiez des messages en français dans fr.lproj/Localizable.strings:

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

    La charge utile du message se présente comme suit:

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

Activer l'exportation des données de diffusion des messages

Vous pouvez exporter vos données de message vers BigQuery pour une analyse plus approfondie. BigQuery vous permet d'analyser les données à l'aide de BigQuery SQL, de les exporter vers un autre fournisseur de services cloud ou de les utiliser pour vos modèles de ML personnalisés. Une exportation vers BigQuery inclut toutes les données disponibles pour les messages, quel que soit leur type ou s'ils sont envoyés via l'API ou l'outil de création de notifications.

Pour activer l'exportation, suivez d'abord la procédure décrite ici, puis suivez ces instructions:

Android

Vous pouvez utiliser le code suivant:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

Pour iOS, vous devez remplacer AppDelegate.m par le contenu suivant.

#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

Pour le Web, vous devez modifier votre service worker afin d'utiliser la version 9 du SDK. La version v9 doit être groupée. Vous devez donc utiliser un outil de regroupement tel que esbuild, par exemple, pour que le service worker fonctionne. Consultez l'exemple d'application pour savoir comment procéder.

Une fois que vous avez migré vers le SDK v9, vous pouvez utiliser le code suivant:

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

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

N'oubliez pas d'exécuter yarn build pour exporter la nouvelle version de votre service worker vers le dossier web.

Afficher des images dans les notifications sur iOS

Sur les appareils Apple, pour que les notifications FCM entrantes affichent des images de la charge utile FCM, vous devez ajouter une extension de service de notification supplémentaire et configurer votre application pour qu'elle l'utilise.

Si vous utilisez l'authentification par téléphone Firebase, vous devez ajouter le pod Firebase Authentication à votre Podfile.

Étape 1 : Ajouter une extension de service de notification

  1. Dans Xcode, cliquez sur File > New > Target (Fichier > Nouveau > Cible).
  2. Une fenêtre modale affiche une liste de cibles possibles. Faites défiler la page vers le bas ou utilisez le filtre pour sélectionner Notification Service Extension (Extension du service de notification). Cliquez sur Suivant.
  3. Ajoutez un nom de produit (utilisez "ImageNotification" pour suivre ce tutoriel), définissez la langue sur Objective-C, puis cliquez sur Finish (Terminer).
  4. Activez le schéma en cliquant sur Activer.

Étape 2 : Ajouter une cible au fichier Podfile

Assurez-vous que votre nouvelle extension a accès au pod Firebase/Messaging en l'ajoutant dans le Podfile:

  1. Dans le navigateur, ouvrez le fichier Podfile: Pods > Podfile

  2. Faites défiler le fichier jusqu'en bas et ajoutez:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Installez ou mettez à jour vos pods à l'aide de pod install à partir du répertoire ios ou macos.

Étape 3 : Utilisez l'assistant d'extension

À ce stade, tout devrait continuer à fonctionner normalement. La dernière étape consiste à appeler l'assistant de l'extension.

  1. Dans le navigateur, sélectionnez votre extension ImageNotification.

  2. Ouvrez le fichier NotificationService.m.

  3. En haut du fichier, importez FirebaseMessaging.h juste après NotificationService.h, comme illustré ci-dessous.

    Remplacez le contenu de NotificationService.m par:

    #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
    

Étape 4 : Ajouter l'image à la charge utile

Vous pouvez désormais ajouter une image dans la charge utile de votre notification. Consultez la documentation iOS pour savoir comment créer une requête d'envoi. N'oubliez pas que la taille maximale de l'image est limitée à 300 Ko par l'appareil.