Ricevi messaggi in un'app Flutter

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

Stato Descrizione
Primo piano Quando l'applicazione è aperta, in vista e in uso.
Premessa Quando l'applicazione è aperta, ma in background (ridotta a icona). Questo accade in genere quando l'utente ha premuto il pulsante "Home" sul dispositivo, ha passato a un'altra app utilizzando il selettore di app o ha aperto l'applicazione in un'altra scheda (web).
Risoluzione Quando il dispositivo è bloccato o l'applicazione non è in esecuzione.

Ci sono alcune condizioni preliminari che devono essere soddisfatte prima che l'applicazione possa ricevi payload dei messaggi tramite FCM:

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

Richiedere l'autorizzazione per ricevere messaggi

Su iOS, macOS, web e Android 13 (o versioni successive), prima che i payload FCM possano essere ricevuti sul tuo dispositivo, devi prima chiedere l'autorizzazione dell'utente.

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

Per iniziare, chiama il metodo dalla tua applicazione (su iOS verrà visualizzata una finestra modale nativa, verrà attivato il flusso API nativo 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
di Gemini Advanced.

Le altre proprietà su NotificationSettings indicano se un'autorizzazione specifica è abilitata, disabilitata o non supportata nella dispositivo.

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

Gestione dei messaggi

In base allo stato attuale della tua applicazione, payload in entrata di diversi tipi di messaggi richiedono implementazioni diverse per gestirle:

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 in dettaglio varie informazioni sul payload, come la provenienza, l'ID univoco, l'ora di invio e la presenza o meno di 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 ricevuti mentre l'applicazione è in primo piano non mostreranno una notifica visibile su entrambi Android e iOS. Tuttavia, è possibile ignorare questo comportamento:

  • Su Android, devi creare un'impostazione di "priorità alta" canale di notifica.
  • 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 native (Android e Apple) e web.

Piattaforme Apple e Android

Puoi gestire i messaggi in background registrando un gestore onBackgroundMessage. Quando i messaggi vengono ricevuti, viene generato isolato (solo Android, iOS/macOS non richiede un'operazione di isolamento separata) in modo da poter gestire i messaggi anche quando l'applicazione non è in esecuzione.

Ci sono alcuni aspetti da tenere in considerazione in merito al 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. Se utilizzi Flutter versione 3.3.0 o successive, il gestore di messaggi deve essere annotato con @pragma('vm:entry-point') subito sopra la dichiarazione della funzione (altrimenti può essere rimosso durante l'agitazione degli alberi 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 in una propria isolata al di fuori del contesto delle applicazioni, non è possibile eseguire l'aggiornamento lo stato dell'applicazione o eseguire qualsiasi interfaccia utente che influisce sulla logica. Tuttavia, è possibile eseguire operazioni logiche, come richieste HTTP, I/O (ad es. aggiornamento dello spazio di archiviazione locale), comunicare con altri plug-in e così via.

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

Web

Sul web, scrivi un Service Worker JavaScript 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 assegnagli il nome 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);
});

Il file deve importare gli SDK sia per le app che per i messaggi, inizializzare Firebase ed esporre la variabile messaging.

Quindi, il worker deve essere registrato. All'interno del file index.html, registra il worker modificando il tag <script> che avvia 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 stai ancora utilizzando il vecchio sistema di modelli, puoi registrare il worker modificando il tag <script> che avvia Flutter nel seguente modo:

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

Quindi riavvia l'applicazione Flutter. Il worker verrà registrato e gli eventuali messaggi in background verranno gestiti tramite questo file.

Interazione con il trattamento

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

A seconda dei contenuti di una notifica, potresti voler gestire l'interazione dell'utente all'apertura dell'applicazione. Ad esempio, se un nuovo messaggio di chat viene inviato tramite 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 terminato, verrà restituito un Future contenente un RemoteMessage. Una volta consumato, RemoteMessage verrà rimosso.
  • onMessageOpenedApp: un Stream che pubblica un RemoteMessage quando l'applicazione viene aperta in uno stato in background.

È consigliabile gestire entrambi gli scenari per garantire un'esperienza utente senza problemi per gli utenti. Il codice di esempio seguente illustra come 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 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("...");
  }
}

Il modo in cui gestisci l'interazione dipende dalla configurazione dell'applicazione. L'esempio riportato sopra mostra un'illustrazione di base dell'utilizzo di StatefulWidget.

Localizza i messaggi

Puoi inviare stringhe localizzate in due modi diversi:

  • Memorizza la lingua preferita di ogni utente sul tuo server e invia notifiche personalizzate per ogni lingua
  • Incorporare stringhe localizzate nella tua app e utilizzare le impostazioni internazionali native 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, invece di 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 sarà simile al seguente:

    {
      "data": {
        "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 dei messaggi sarà simile al seguente:

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

Abilita esportazione dati per la consegna dei messaggi

Puoi esportare i dati dei messaggi in BigQuery per ulteriori analisi. BigQuery ti consente di analizzare i dati utilizzando BigQuery SQL, di esportarli in un altro provider cloud o di utilizzarli per i tuoi modelli 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 tramite l'API o il compositore Notifications.

Per attivare l'esportazione, segui innanzitutto i passaggi descritti qui, segui queste istruzioni:

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 tuo service worker per utilizzare la versione 9 dell'SDK. La versione v9 deve essere in bundle, quindi devi utilizzare un bundler come esbuild ad esempio per far funzionare il service worker. Consulta l'app di esempio per scoprire come farlo.

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.

Visualizza le immagini nelle notifiche su iOS

Sui dispositivi Apple, per consentire alle notifiche FCM in arrivo di visualizzare le immagini dal payload FCM, devi aggiungere un'estensione del servizio di notifica aggiuntiva e configurare l'app per usarla.

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

Passaggio 1: aggiungi un'estensione del servizio di notifica

  1. In Xcode, fai clic su File > Nuovo > Obiettivo...
  2. Una finestra modale presenterà un elenco di possibili target. Scorri verso il basso o utilizza il filtro per selezionare Notification Service Extension. Fai clic su Avanti.
  3. Aggiungi un nome per il prodotto (utilizza "ImageNotification" per seguire questo tutorial), imposta la lingua su Objective-C e fai clic su Fine.
  4. Attiva lo schema facendo clic su Attiva.

Passaggio 2 - Aggiungi la destinazione al podfile

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

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

  2. Scorri fino 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 delle estensioni

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

  1. Nel navigatore, seleziona l'estensione ImageNotification.

  2. Apri il file NotificationService.m.

  3. Nella parte superiore del file, importa FirebaseMessaging.h subito dopo NotificationService.h, come mostrato di seguito.

    Sostituisci i contenuti di NotificationService.m con:

    #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
    

Passaggio 4 - Aggiungi l'immagine al payload

Ora puoi aggiungere un'immagine nel payload per le notifiche. Consulta la documentazione iOS su come creare una richiesta di invio. Tieni presente che il dispositivo applica una dimensione massima dell'immagine di 300 KB.