Odbieranie wiadomości w aplikacji Flutter

W zależności od stanu urządzenia wiadomości przychodzące są obsługiwane inaczej. Aby zrozumieć te scenariusze i sposób integracji FCM z własną aplikacją, musisz najpierw określić różne stany, w których może się znajdować urządzenie:

Stan Opis
Pierwszy plan gdy aplikacja jest otwarta, widoczna i używana;
Tło Gdy aplikacja jest otwarta, ale w tle (zminimalizowana). Zwykle dzieje się tak, gdy użytkownik naciśnie przycisk „Strona główna” na urządzeniu, przełączy się na inną aplikację za pomocą przełącznika aplikacji lub otworzy aplikację na innej karcie (w przypadku przeglądarki).
Zakończona gdy urządzenie jest zablokowane lub aplikacja jest zamknięta.

Zanim aplikacja będzie mogła odbierać ładunki wiadomości za pomocą FCM, musi zostać spełnionych kilka warunków wstępnych:

  • Aplikacja musi zostać otwarta co najmniej raz (aby umożliwić rejestrację w FCM).
  • Jeśli użytkownik przesunie aplikację poza przełącznik aplikacji w systemie iOS, trzeba będzie ponownie otworzyć ją ręcznie, aby wiadomości w tle zaczęły działać.
  • Jeśli użytkownik wymusi zamknięcie aplikacji na urządzeniu z Androidem w ustawieniach urządzenia, aby wiadomości zaczęły działać, trzeba ponownie otworzyć ją ręcznie.
  • W przeglądarce musisz wysłać prośbę o token (przy użyciu getToken()) za pomocą certyfikatu Web Push Certificate.

Prośba o dostęp do odbierania wiadomości

W systemach iOS, macOS, web i Android 13 (lub nowszym) przed otrzymaniem na urządzeniu danych Firebase Cloud Messaging musisz najpierw poprosić użytkownika o zgodę.

Pakiet firebase_messaging udostępnia prosty interfejs API do żądania uprawnień za pomocą metody requestPermission. Ten interfejs API akceptuje szereg argumentów nazwanych określających typ uprawnień, których dotyczy żądanie, na przykład to, czy przesyłanie informacji zawierających ładunki powiadomień może wywołać dźwięk lub odczytywać wiadomości za pomocą Siri. Domyślnie metoda prosi o domyślne uprawnienia. W dokumentacji interfejsu API znajdziesz pełną dokumentację dotyczącą każdego uprawnienia.

Aby rozpocząć, wywołaj metodę z aplikacji (w iOS wyświetli się natywny proces modalny, a w internecie zostanie uruchomiony natywny przepływ interfejsu API przeglądarki):

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}');

Właściwości authorizationStatus obiektu NotificationSettings zwróconego w żądaniu można użyć do określenia ogólnej decyzji użytkownika:

  • authorized: użytkownik udzielił uprawnień.
  • denied: użytkownik odmówił udzielenia uprawnień.
  • notDetermined: użytkownik nie podjął jeszcze decyzji, czy udzielić uprawnień.
  • provisional: użytkownik udzielił tymczasowego pozwolenia

Pozostałe właściwości elementu NotificationSettings określają, czy określone uprawnienie jest włączone, wyłączone czy nie jest obsługiwane na bieżącym urządzeniu.

Gdy użytkownik udzieli zgody, a aplikacja rozpozna różne stany urządzenia, będzie mogła zacząć obsługiwać przychodzące dane FCM.

Obsługa wiadomości

W zależności od bieżącego stanu aplikacji przychodzące dane ładunku o różnych typach wiadomości wymagają różnych implementacji:

Komunikaty na pierwszym planie

Aby obsługiwać wiadomości, gdy aplikacja działa na pierwszym planie, nasłuchuj strumienia 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}');
  }
});

Strumień zawiera RemoteMessage, który zawiera różne informacje o danych, takie jak ich pochodzenie, unikalny identyfikator, czas wysłania, czy zawierały powiadomienie itp. Ponieważ wiadomość została pobrana, gdy aplikacja była na pierwszym planie, możesz bezpośrednio uzyskać dostęp do stanu i kontekstu aplikacji Flutter.

Wiadomości na pierwszym planie i powiadomienia

Powiadomienia, które docierają, gdy aplikacja działa na pierwszym planie, nie będą wyświetlane domyślnie na Androidzie ani na iOS. Można jednak zmienić to zachowanie:

  • Na Androidzie musisz utworzyć kanał powiadomień o wysokim priorytecie.
  • W iOS możesz zaktualizować opcje prezentacji aplikacji.

wiadomości w tle,

Proces obsługi wiadomości w tle różni się na platformach natywnych (Android i Apple) oraz na platformach internetowych.

Platformy Apple i Android

Obsługuj wiadomości w tle przez zarejestrowanie modułu obsługi onBackgroundMessage. Po odebraniu wiadomości generowany jest izolowany element (tylko na Androidzie, iOS/macOS nie wymaga osobnego izolacji), który umożliwia obsługę wiadomości nawet wtedy, gdy aplikacja nie jest uruchomiona.

Kilka kwestii, o których warto pamiętać w przypadku modułu obsługi wiadomości w tle:

  1. Nie może to być funkcja anonimowa.
  2. Musi to być funkcja najwyższego poziomu (np. nie jest to metoda klasy, która wymaga inicjowania).
  3. Jeśli używasz Fluttera w wersji 3.3.0 lub nowszej, uchwyt wiadomości musi być opatrzony adnotacją @pragma('vm:entry-point') tuż nad deklaracją funkcji (w przeciwnym razie może zostać usunięty podczas wstrząsania drzewem w trybie wydania).
@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());
}

Ponieważ moduł obsługi działa w własnym odizolowaniu poza kontekstem aplikacji, nie jest możliwe aktualizowanie stanu aplikacji ani wykonywanie żadnego interfejsu użytkownika wpływającego na logikę. Możesz jednak wykonywać operacje logiczne, takie jak żądania HTTP, operacje wejścia/wyjścia (np. aktualizowanie lokalnego magazynu danych) czy komunikowanie się z innymi wtyczkami.

Zalecamy też jak najszybsze uzupełnienie reguły. Długie, intensywne zadania wpływają na wydajność urządzenia i mogą spowodować zakończenie procesu przez system operacyjny. Jeśli zadania trwają dłużej niż 30 sekund, urządzenie może automatycznie przerwać proces.

Sieć

W przeglądarce napisz skrypt JavaScript Service Worker, który działa w tle. Do obsługi wiadomości w tle używaj usługi workera.

Na początek utwórz nowy plik w katalogu web i nazwij go 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);
});

Plik musi zaimportować zarówno pakiet SDK aplikacji, jak i pakiety SDK do przesyłania wiadomości, zainicjować Firebase i udostępnić zmienną messaging.

Następnie pracownik musi zostać zarejestrowany. W pliku index.html zarejestruj instancję roboczą, modyfikując tag <script>, który wczytuje 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>

Jeśli nadal używasz starego systemu szablonów, możesz zarejestrować instancję roboczą, modyfikując tag <script>, który wczytuje Flutter w ten sposób:

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

Następnie uruchom ponownie aplikację Flutter. Pracownik zostanie zarejestrowany, a wszystkie wiadomości w tle będą obsługiwane za pomocą tego pliku.

Radzenie sobie z interakcją

Powiadomienia są widoczne, dlatego użytkownicy często wchodzą z nimi w interakcję (naciskając je). Domyślnie zarówno na Androidzie, jak i w iOS otwiera się aplikacja. Jeśli aplikacja została zamknięta, zostanie uruchomiona. Jeśli działa w tle, zostanie przeniesiona na pierwszy plan.

W zależności od treści powiadomienia możesz chcieć obsłużyć interakcję użytkownika podczas otwierania aplikacji. Jeśli na przykład nowa wiadomość na czacie zostanie wysłana za pomocą powiadomienia, a użytkownik kliknie je, możesz otworzyć konkretną rozmowę po otwarciu aplikacji.

Pakiet firebase-messaging udostępnia 2 sposoby obsługi tej interakcji:

  • getInitialMessage(): jeśli aplikacja zostanie otwarta z zamkniętego stanu, zwrócony zostanie obiekt Future zawierający obiekt RemoteMessage. Po wykorzystaniu RemoteMessage zostanie usunięty.
  • onMessageOpenedApp: Stream, który publikuje RemoteMessage, gdy aplikacja jest otwarta w tle.

Zalecamy uwzględnienie obu scenariuszy, aby zapewnić użytkownikom płynne wrażenia. Przykładowy kod poniżej pokazuje, jak to zrobić:

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("...");
  }
}

Sposób obsługi interakcji zależy od konfiguracji aplikacji. Powyższy przykład przedstawia podstawową ilustrację z użyciem komponentu StatefulWidget.

Lokalizacja wiadomości

Lokalizowane ciągi tekstowe możesz wysyłać na 2 sposoby:

  • Zapisywanie preferowanego języka każdego użytkownika na serwerze i wysyłanie dostosowanych powiadomień w każdym języku
  • umieszczać w aplikacji zlokalizowane ciągi tekstowe i korzystać z ustawień języka domyślnego systemu operacyjnego;

Aby skorzystać z drugiej metody:

Android

  1. Określ komunikaty w domyślnym języku w pliku resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Określ przetłumaczone wiadomości w katalogu values-language. Na przykład możesz określić wiadomości w języku francuskim w elementach resources/values-fr/strings.xml:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. W ładunku serwera zamiast kluczy title, message i body użyj title_loc_key i body_loc_key w przypadku zlokalizowanej wiadomości i ustaw je na atrybut name wiadomości, którą chcesz wyświetlić.

    Treść wiadomości będzie wyglądać tak:

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

iOS

  1. Określ komunikaty w domyślnym języku w pliku Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Określ przetłumaczone wiadomości w katalogu language.lproj. Na przykład wiadomości w języku francuskim w fr.lproj/Localizable.strings:

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

    Wiadomość będzie wyglądać tak:

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

Włączanie eksportu danych o dostawie wiadomości

Dane wiadomości możesz wyeksportować do BigQuery na potrzeby dalszej analizy. BigQuery umożliwia analizowanie danych za pomocą BigQuery SQL, eksportowanie ich do innego dostawcy chmury oraz wykorzystywanie danych na potrzeby niestandardowych modeli ML. Eksport do BigQuery obejmuje wszystkie dostępne dane dotyczące wiadomości, niezależnie od ich typu i od tego, czy zostały wysłane za pomocą interfejsu API czy edytora powiadomień.

Aby włączyć eksportowanie, wykonaj najpierw czynności opisane tutaj, a potem wykonaj te instrukcje:

Android

Możesz użyć tego kodu:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

W przypadku iOS musisz zastąpić AppDelegate.m tymi treściami.

#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

Sieć

Aby korzystać z wersji 9 pakietu SDK, musisz zmienić usługę wtyczki. Wersja 9 musi być dołączana do pakietu, więc tak aby skrypt service worker działał prawidłowo, np. musisz użyć narzędzia do tworzenia pakietów, takiego jak esbuild. Aby dowiedzieć się, jak to zrobić, zapoznaj się z przykładową aplikacją.

Po migracji na pakiet SDK w wersji 9 możesz użyć tego kodu:

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

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

Nie zapomnij uruchomić yarn build, aby wyeksportować nową wersję skryptu service worker do folderu web.

Wyświetlanie obrazów w powiadomieniach na iOS

Aby na urządzeniach Apple przychodzące powiadomienia FCM wyświetlały obrazy z załadunku FCM, musisz dodać dodatkowe rozszerzenie usługi powiadomień i skonfigurować aplikację tak, aby z niego korzystała.

Jeśli korzystasz z uwierzytelniania przez telefon w Firebase, musisz dodać moduł Firebase Auth do pliku Podfile.

Krok 1. Dodaj rozszerzenie usługi powiadomień

  1. W Xcode kliknij Plik > Nowy > Docelowy….
  2. Pojawi się modalna lista możliwych elementów docelowych. Przewiń w dół lub użyj filtra, aby wybrać Rozszerzenie usługi powiadomień. Kliknij Dalej.
  3. Dodaj nazwę produktu (użyj „ImageNotification”, aby łatwiej było Ci śledzić ten samouczek), ustaw język na Objective-C i kliknij Zakończ.
  4. Aby włączyć schemat, kliknij Aktywuj.

Krok 2. Dodaj do pliku Podfile element docelowy

Upewnij się, że nowe rozszerzenie ma dostęp do podu Firebase/Messaging, dodając je w pliku Podfile:

  1. W nawigatorze otwórz plik Podfile: Pods > Podfile.

  2. Przewiń do dołu pliku i dodaj:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Zainstaluj lub zaktualizuj swoje pody, używając polecenia pod install z katalogu ios lub macos.

Krok 3. Użyj narzędzia do pomocy w rozszerzeniu

W tej chwili wszystko powinno działać normalnie. Ostatnim krokiem jest wywołanie pomocy dotyczącej rozszerzenia.

  1. W nawigatorze wybierz rozszerzenie ImagePowiadomienie.

  2. Otwórz plik NotificationService.m.

  3. U góry pliku zaimportuj FirebaseMessaging.h tuż po NotificationService.h, jak pokazano poniżej.

    Zamień zawartość pliku NotificationService.m na:

    #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
    

Krok 4. Dodaj obraz do ładunku

Teraz możesz dodawać obrazy do ładunku powiadomienia. Informacje o tworzeniu próśb o wysłanie wiadomości znajdziesz w dokumentacji iOS. Pamiętaj, że na urządzeniu obowiązuje maksymalny rozmiar obrazu wynoszący 300 KB.