Recevoir des messages dans une application Flutter

Selon l'état de l'appareil, les messages entrants sont traités différemment. À comprendre ces scénarios et apprendre à intégrer FCM à votre propre application, est d'abord important d'établir les différents états possibles d'un appareil:

État Description
Premier plan Lorsque l'application est ouverte, visible et en cours d'utilisation.
Contexte Lorsque l'application est ouverte, mais en arrière-plan (réduite). Cela se produit généralement lorsque l'utilisateur appuie sur le bouton d'accueil bouton sur l'appareil, est passé à une autre application à l'aide du sélecteur d'applications, ou si l'application est ouverte dans un autre onglet (Web).
Arrêtée Lorsque l'appareil est verrouillé ou que l'application n'est pas en cours d'exécution.

Quelques conditions préalables doivent être remplies pour que l'application puisse recevoir les charges utiles des messages via FCM:

  • L'application doit avoir été ouverte au moins une fois (pour permettre l'enregistrement auprès de FCM).
  • Sur iOS, si l'utilisateur fait glisser l'application en dehors du sélecteur d'applications, elle doit être rouverte manuellement pour que les messages en arrière-plan fonctionnent à nouveau.
  • 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 Web push.

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 les charges utiles de notification peuvent déclencher un son ou lire des messages à voix haute via Siri. Par défaut, la méthode demande des autorisations par défaut raisonnables. L'API de référence fournit une documentation complète sur l'utilité de chaque autorisation.

Pour commencer, appelez la méthode depuis votre application (sur iOS, une fenêtre modale native s'affiche, le flux de l'API native 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 décidé d'accorder ou non 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 compatible avec la version actuelle appareil.

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

Gestion des messages

Selon 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 détaillant diverses informations sur la charge utile, telles que son origine, l'identifiant unique, l'heure d'envoi, si elle contenait ou non une notification et plus encore. Comme le message a été récupéré alors que votre application était au premier plan, vous pouvez accéder directement à votre l'état et le contexte de l'application.

Messages de premier plan et de notification

Les messages de notification qui arrivent lorsque l'application est au premier plan n'affichent pas de notification visible par défaut, sur les deux Android et iOS. Il est toutefois possible d'ignorer ce comportement:

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

Messages en arrière-plan

Le processus de gestion des messages en arrière-plan est différent pour les annonces natives (Android et Apple) et des plates-formes 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 à garder à l'esprit 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. Si vous utilisez Flutter version 3.3.0 ou ultérieure, le gestionnaire de messages doit être annoté avec @pragma('vm:entry-point') juste au-dessus de la déclaration de la fonction (sinon, il risque d'être supprimé lors de l'arborescence de l'application en mode release).
@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 élément isolé en dehors du contexte de votre application, il n'est pas possible de mettre à jour l'état de l'application ou exécuter une logique ayant un impact sur l'UI. Vous pouvez toutefois exécuter des logiques telles que des requêtes HTTP, des opérations d'E/S (par exemple, mettre à jour le stockage local), communiquer 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 affecte les performances de l'appareil et peut amener le système d’exploitation à mettre fin au processus. Si des tâches s'exécutent 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 le 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.

Le nœud de calcul doit ensuite être enregistré. Dans le fichier index.html, enregistrez le nœud de calcul en modifiant la balise <script> qui amorce 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

Les notifications étant un signal visible, il est courant que les utilisateurs interagissent avec elles (en appuyant dessus). Par défaut sur Android et iOS, l'application s'ouvre 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.

Selon le contenu d'une notification, vous souhaiterez peut-être gérer l'interaction de l'utilisateur à l'ouverture de l'application. Par exemple, si un nouveau message de chat est envoyé par notification et que l'utilisateur appuie dessus, vous souhaiterez peut-être ouvrir la conversation spécifique à l'ouverture de l'application.

Le package firebase-messaging propose deux manières de gérer cette interaction:

  • getInitialMessage(): si l'application est ouverte à partir d'un état d'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 en arrière-plan.

Nous vous recommandons de gérer ces deux scénarios afin de garantir une expérience utilisateur fluide pour vos utilisateurs. L'exemple de code ci-dessous montre comment y parvenir:

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 les interactions dépend de la configuration de votre application. L'exemple ci-dessus illustre l'utilisation d'un StatefulWidget.

Localiser les messages

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

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

Voici comment utiliser la deuxième méthode:

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 les 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 distribution des messages

Vous pouvez exporter les données de vos messages vers BigQuery pour une analyse plus approfondie. BigQuery vous permet d'analyser les données à l'aide de BigQuery SQL, les exporter vers un autre fournisseur de services cloud ou 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 leur envoi via des l'API ou l'outil de création de notifications.

Pour activer l'exportation, suivez d'abord les étapes décrites 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 v9 du SDK. La version v9 doit être intégrée. Vous devez donc utiliser un bundler comme esbuild, par exemple. pour faire fonctionner le service worker. Pour savoir comment procéder, consultez l'exemple d'application.

Une fois que vous avez migré vers la version 9 du SDK, 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 les images dans les notifications sur iOS

Sur les appareils Apple, pour que les notifications FCM entrantes affichent des images à partir 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 Auth à votre Podfile.

Étape 1 : Ajoutez une extension de service de notification

  1. Dans Xcode, cliquez sur File > Nouveau > Cibler...
  2. Une fenêtre modale présente la liste des 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 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 au Podfile:

  1. Dans le navigateur, ouvrez le Podfile: Pods > Fichier Pod

  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 de fonctionner normalement. La dernière étape consiste à appeler l'assistant d'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 indiqué 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 votre charge utile de 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.