Ricezione di messaggi nelle app Flutter

Seleziona la piattaforma: iOS+ Android Web Flutter Unity C++


A seconda dello stato di un dispositivo, i messaggi in arrivo vengono gestiti in modo diverso. Per comprendere questi scenari e come integrare FCM nella tua applicazione, è importante innanzitutto stabilire i vari stati in cui può trovarsi un dispositivo:

Stato Descrizione
Primo piano Quando l'applicazione è aperta, in visualizzazione e in uso.
Sfondo Quando l'applicazione è aperta, ma in background (ridotta a icona). In genere, questo accade quando l'utente ha premuto il pulsante "Home" sul dispositivo, ha cambiato app utilizzando il selettore di app o ha aperto l'applicazione in una scheda diversa (web).
Risolto in data Quando il dispositivo è bloccato o l'applicazione non è in esecuzione.

Prima che l'applicazione possa ricevere i payload dei messaggi utilizzando FCM, devono essere soddisfatte alcune precondizioni:

  • L'applicazione deve essere stata aperta almeno una volta (per consentire la registrazione con FCM).
  • Su iOS, se l'utente chiude l'applicazione dal selettore di app, deve riaprirla manualmente affinché i messaggi in background inizino a funzionare di nuovo.
  • Su Android, se l'utente chiude forzatamente l'app dalle impostazioni del dispositivo, deve riaprirla manualmente affinché i messaggi inizino a funzionare.
  • Sul web, devi aver richiesto un token (utilizzando getToken()) con il certificato push web.

Richiedi l'autorizzazione a ricevere messaggi

Su iOS, macOS, web e Android 13 (o versioni successive), prima di poter ricevere i payload FCM sul dispositivo, devi prima chiedere l'autorizzazione all'utente.

Il pacchetto firebase_messaging fornisce un'API per richiedere l'autorizzazione utilizzando il requestPermission metodo. Questa API accetta una serie di argomenti denominati che definiscono il tipo di autorizzazioni che vuoi richiedere, ad esempio se la messaggistica contenente payload di notifica può attivare un suono o leggere i messaggi utilizzando Siri. Per impostazione predefinita, il metodo richiede autorizzazioni predefinite sensibili. L'API di riferimento fornisce la documentazione completa sullo scopo di ogni autorizzazione.

Per iniziare, chiama il metodo dalla tua applicazione (su iOS verrà visualizzata una finestra modale integrata, sul web verrà attivato il flusso dell'API del browser):

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 proprietà authorizationStatus dell'oggetto NotificationSettings restituito dalla richiesta può essere utilizzata per determinare la decisione complessiva dell'utente:

  • authorized: l'utente ha concesso l'autorizzazione.
  • denied: l'utente ha negato l'autorizzazione.
  • notDetermined: l'utente non ha ancora scelto se concedere l'autorizzazione.
  • provisional: l'utente ha concesso l'autorizzazione provvisoria.

Le altre proprietà di NotificationSettings indicano se un'autorizzazione specifica è attivata, disattivata o non supportata sul dispositivo corrente.

Una volta concessa l'autorizzazione e compresi i diversi tipi di stato del dispositivo, l'applicazione può iniziare a gestire i payload FCM in arrivo.

Gestione dei messaggi

A seconda dello stato attuale dell'applicazione, i payload in arrivo di diversi tipi di messaggi richiedono implementazioni diverse per la gestione:

Messaggi in primo piano

Per gestire i messaggi mentre l'applicazione è in primo piano, ascolta lo stream 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}');
  }
});

Lo stream contiene un RemoteMessage, che descrive varie informazioni sul payload, ad esempio la provenienza, l'ID univoco, l'ora di invio, se conteneva una notifica e altro ancora. Poiché il messaggio è stato recuperato mentre l'applicazione è in primo piano, puoi accedere direttamente allo stato e al contesto dell'applicazione Flutter.

Messaggi in primo piano e di notifica

Per impostazione predefinita, i messaggi di notifica che arrivano mentre l'applicazione è in primo piano non mostrano una notifica visibile, sia su Android che su iOS. Tuttavia, è possibile eseguire l'override di questo comportamento:

  • Su Android, devi creare un canale di notifica "Priorità alta".
  • Su iOS, puoi aggiornare le opzioni di presentazione per l'applicazione.

Messaggi in background

La procedura di gestione dei messaggi in background è diversa sulle piattaforme basate su Android, Apple e web.

Piattaforme Apple e Android

Gestisci i messaggi in background registrando un gestore onBackgroundMessage. Quando vengono ricevuti i messaggi, viene generato un isolato (solo Android, iOS/macOS non richiede un isolato separato) che ti consente di gestire i messaggi anche quando l'applicazione non è in esecuzione.

Ecco alcuni aspetti da tenere presente sul gestore dei messaggi in background:

  1. Non deve essere una funzione anonima.
  2. Deve essere una funzione di primo livello (ad es. non un metodo di classe che richiede l'inizializzazione).
  3. Quando utilizzi Flutter versione 3.3.0 o successive, il gestore dei messaggi deve essere annotato con @pragma('vm:entry-point') subito sopra la dichiarazione della funzione (altrimenti potrebbe essere rimosso durante l'eliminazione del codice inutilizzato per la modalità di rilascio).
@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());
}

Poiché il gestore viene eseguito nel proprio isolato al di fuori del contesto delle applicazioni, non è possibile aggiornare lo stato dell'applicazione o eseguire qualsiasi logica che influisce sull'UI. Tuttavia, puoi eseguire logiche come richieste HTTP, operazioni di I/O (ad es. aggiornamento dell'archiviazione locale), comunicare con altri plug-in e così via.

Ti consigliamo inoltre di completare la logica il prima possibile. L'esecuzione di attività lunghe e intensive influisce sulle prestazioni del dispositivo e potrebbe causare la chiusura del processo da parte del sistema operativo. Se le attività vengono eseguite per più di 30 secondi, il dispositivo potrebbe chiudere automaticamente il processo.

Web

Sul web, scrivi un service worker JavaScript Service Worker che viene eseguito in background. Utilizza il service worker per gestire i messaggi in background.

Per iniziare, crea un nuovo file nella directory web e chiamalo firebase-messaging-sw.js:

// See this file for the latest firebase-js-sdk version:
// https://github.com/firebase/flutterfire/blob/main/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);
});

Il file deve importare gli SDK dell'app e della messaggistica, inizializzare Firebase ed esporre la variabile messaging.

Poi, il worker deve essere registrato. Nel file index.html, registra il worker modificando il tag <script> che esegue il bootstrap di 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>

Se utilizzi ancora il vecchio sistema di modelli, puoi registrare il worker modificando il tag <script> che esegue il bootstrap di Flutter come segue:

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

Riavvia l'applicazione Flutter. Il worker verrà registrato e tutti i messaggi in background verranno gestiti utilizzando questo file.

Gestione dell'interazione

Poiché le notifiche sono un segnale visibile, è normale che gli utenti interagiscano con loro (premendo). Il comportamento predefinito sia su Android che su iOS è l'apertura dell'applicazione. Se l'applicazione viene chiusa, verrà avviata; se è in background, verrà portata in primo piano.

A seconda del contenuto di una notifica, potresti voler gestire l'interazione dell'utente all'apertura dell'applicazione. Ad esempio, se viene inviato un nuovo messaggio di chat utilizzando una notifica e l'utente la preme, potresti voler aprire la conversazione specifica all'apertura dell'applicazione.

Il pacchetto firebase-messaging offre due modi per gestire questa interazione:

  • getInitialMessage(): se l'applicazione viene aperta da uno stato di chiusura, verrà restituito un Future contenente un RemoteMessage. Una volta utilizzato, il RemoteMessage verrà rimosso.
  • onMessageOpenedApp: uno Stream che pubblica un RemoteMessage quando l'applicazione viene aperta da uno stato in background.

Ti consigliamo di gestire entrambi gli scenari per garantire un'esperienza utente fluida per i tuoi utenti. Il seguente esempio di codice illustra come è possibile ottenere questo risultato:

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 using 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 modalità di gestione dell'interazione dipende dalla configurazione dell'applicazione. L'esempio precedente mostra un'illustrazione di base che utilizza un StatefulWidget.

Localizzare i messaggi

Puoi inviare stringhe localizzate in due modi diversi:

  • Memorizza la lingua preferita di ogni utente sul server e invia notifiche personalizzate per ogni lingua.
  • Incorpora le stringhe localizzate nell'app e utilizza le impostazioni internazionali integrate del sistema operativo.

Ecco come utilizzare il secondo metodo:

Android

  1. Specifica i messaggi nella lingua predefinita in resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Specifica i messaggi tradotti nella directory values-language. Ad esempio, specifica i messaggi in francese in resources/values-fr/strings.xml:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. Nel payload del server, anziché utilizzare le chiavi title, message, e body, utilizza title_loc_key e body_loc_key per il messaggio localizzato e impostale sull'attributo name del messaggio che vuoi visualizzare.

    Il payload del messaggio avrà il seguente aspetto:

    {
      "android": {
         "notification": {
           "title_loc_key": "notification_title",
           "body_loc_key": "notification_message"
         }
      }
    }
    

iOS

  1. Specifica i messaggi nella lingua predefinita in Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Specifica i messaggi tradotti nella directory language.lproj. Ad esempio, specifica i messaggi in francese in fr.lproj/Localizable.strings:

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

    Il payload del messaggio avrà il seguente aspetto:

    {
      "apns": {
         "payload": {
           "alert": {
             "title-loc-key": "NOTIFICATION_TITLE",
             "loc-key": "NOTIFICATION_MESSAGE"
           }
         }
      }
    }
    

Attiva l'esportazione dei dati di recapito dei messaggi

Puoi esportare i dati dei messaggi in BigQuery per ulteriori analisi. BigQuery ti consente di analizzare i dati utilizzando BigQuery SQL, esportarli in un altro provider di servizi cloud o utilizzarli per i tuoi modelli di ML personalizzati. Un'esportazione in BigQuery include tutti i dati disponibili per i messaggi, indipendentemente dal tipo di messaggio o dal fatto che il messaggio venga inviato utilizzando l'API o il compositore di notifiche.

Per attivare l'esportazione, segui prima i passaggi descritti nel documento Esportazione dei dati di BigQuery. L'attivazione programmatica a livello di istanza dell'app ti consente di chiedere agli utenti finali l'autorizzazione ad analizzare i dati di recapito dei messaggi (opzione consigliata). Segui queste istruzioni per attivare l'esportazione a livello di programmazione:

Android

Puoi utilizzare il seguente codice:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

Per iOS, devi modificare AppDelegate.m con i seguenti contenuti.

#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

Per il web, devi modificare il service worker per utilizzare la versione 9 dell'SDK. La versione 9 deve essere raggruppata, quindi devi utilizzare un bundler come esbuild per far funzionare il service worker. Consulta l'app di esempio per scoprire come fare.

Una volta eseguita la migrazione all'SDK v9, puoi utilizzare il seguente codice:

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

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

Non dimenticare di eseguire yarn build per esportare la nuova versione del service worker nella cartella web.

Visualizzare le immagini nelle notifiche su iOS

Sui dispositivi Apple, affinché le notifiche FCM in arrivo visualizzino le immagini dal payload FCM, devi aggiungere un'estensione del servizio di notifica aggiuntiva e configurare l'app in modo che la utilizzi.

Se utilizzi l'autenticazione telefonica Firebase, devi aggiungere il pod Firebase Auth al podfile.

Passaggio 1: aggiungi un'estensione del servizio di notifica

  1. In Xcode, fai clic su File > New > Target... (File > Nuovo > Destinazione…)
  2. Verrà visualizzata una finestra modale con un elenco di possibili destinazioni; scorri o utilizza il filtro per selezionare Notification Service Extension (Estensione del servizio di notifica). Fai clic su Avanti.
  3. Aggiungi un nome del prodotto (utilizza "ImageNotification" per seguire questo tutorial), seleziona Swift o Objective-C e fai clic su Fine.
  4. Attiva lo schema facendo clic su Attiva.

Passaggio 2: aggiungi la destinazione al podfile

Swift

Assicurati che la nuova estensione abbia accesso al pacchetto Swift FirebaseMessaging aggiungendolo alla destinazione Runner:

  1. Dal navigatore, aggiungi l'SDK delle piattaforme Apple Firebase: File > Aggiungi dipendenze pacchetto…

  2. Cerca o inserisci l'URL del pacchetto: none https://github.com/firebase/firebase-ios-sdk

  3. Aggiungi a Project Runner: Add Package (Aggiungi pacchetto)

  4. Scegli FirebaseMessaging e aggiungilo alla destinazione ImageNotification: Add Package (Aggiungi pacchetto)

Objective-C

Assicurati che la nuova estensione abbia accesso al pod Firebase/Messaging aggiungendolo al podfile:

  1. Dal navigatore, apri il podfile: Pods > Podfile

  2. Vai in fondo al file e aggiungi:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Installa o aggiorna i pod utilizzando pod install dalla directory ios o macos.

Passaggio 3: utilizza l'helper dell'estensione

A questo punto, tutto dovrebbe funzionare normalmente. Il passaggio finale consiste nell'invocare l'helper dell'estensione.

Swift

  1. Dal navigatore, seleziona l'estensione ImageNotification

  2. Apri il file NotificationService.swift.

  3. Sostituisci i contenuti di NotificationService.swift con:

    import UserNotifications
    import FirebaseMessaging
    
    class NotificationService: UNNotificationServiceExtension {
    
        var contentHandler: ((UNNotificationContent) -> Void)?
        var bestAttemptContent: UNMutableNotificationContent?
    
        override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
            self.contentHandler = contentHandler
            bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    
            Messaging.serviceExtension().populateNotificationContent(bestAttemptContent!, withContentHandler: contentHandler)
        }
    
        override func serviceExtensionTimeWillExpire() {
            if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
                contentHandler(bestAttemptContent)
            }
        }
    }
    

Objective-C

  1. Dal navigatore, seleziona l'estensione ImageNotification

  2. Apri il file NotificationService.m.

  3. All'inizio del file, importa FirebaseMessaging.h subito dopo NotificationService.h.

    Sostituisci i contenuti di NotificationService.m con:

    #import "NotificationService.h"
    #import "FirebaseMessaging.h"
    #import <FirebaseAuth/FirebaseAuth-Swift.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 () <NSURLSessionDelegate>
    
    @property(nonatomic) void (^contentHandler)(UNNotificationContent *contentToDeliver);
    @property(nonatomic) 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
    

Passaggio 4: aggiungi l'immagine al payload

Nel payload della notifica, ora puoi aggiungere un'immagine. Per saperne di più, scopri come creare una richiesta di invio.