Odbieranie wiadomości w aplikacji Flutter

Wiadomości przychodzące są obsługiwane w różny sposób w zależności od stanu urządzenia. Aby zrozumieć te scenariusze i dowiedzieć się, jak zintegrować FCM z własną aplikacją, najpierw musisz poznać różne stany, w jakich może znajdować się urządzenie:

Stan Opis
Pierwszy plan Gdy aplikacja jest otwarta, widoczna i używana.
Informacje ogólne Gdy aplikacja jest otwarta, ale działa w tle (jest zminimalizowana). Zwykle dzieje się tak, gdy użytkownik naciśnie przycisk ekranu głównego na urządzeniu, przełączy się na inną aplikację za pomocą przełącznika aplikacji lub otworzy aplikację w innej karcie (w internecie).
Zakończono Gdy urządzenie jest zablokowane lub aplikacja nie jest uruchomiona.

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

  • Aplikacja musi zostać otwarta co najmniej raz (aby umożliwić rejestrację w FCM).
  • Jeśli użytkownik przesunie aplikację w przełączniku aplikacji na iOS, musi ją ponownie otworzyć, aby wiadomości w tle zaczęły znowu działać.
  • Jeśli użytkownik na Androidzie wymusi zamknięcie aplikacji w ustawieniach urządzenia, musi ją ponownie otworzyć, aby wiadomości zaczęły działać.
  • W przypadku internetu musisz poprosić o token (za pomocą getToken()) z certyfikatem powiadomień push w internecie.

Prośba o uprawnienia do odbierania wiadomości

Na urządzeniach z iOS, macOS, Androidem 13 (lub nowszym) i w internecie, zanim na urządzeniu będzie można odbierać ładunki FCM, musisz najpierw poprosić użytkownika o zezwolenie.

Pakiet firebase_messaging udostępnia prosty interfejs API do wysyłania próśb o uprawnienia za pomocą metody requestPermission. Ten interfejs API akceptuje wiele nazwanych argumentów, które określają typ uprawnień, o które chcesz poprosić, np. czy wiadomości zawierające ładunki powiadomień mogą wywoływać dźwięk lub odczytywać wiadomości za pomocą Siri. Domyślnie ta metoda prosi o rozsądne uprawnienia domyślne. Interfejs API zawiera pełną dokumentację dotyczącą każdego uprawnienia.

Aby rozpocząć, wywołaj metodę z aplikacji (w iOS wyświetli się natywny moduł, a w internecie zostanie uruchomiony natywny interfejs 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ść authorizationStatus obiektu NotificationSettings zwróconego przez żądanie może służyć do określania ogólnej decyzji użytkownika:

  • authorized: użytkownik przyznał uprawnienia.
  • denied: użytkownik odmówił przyznania uprawnień.
  • notDetermined: użytkownik nie zdecydował jeszcze, czy chce udzielić zgody.
  • provisional: użytkownik przyznał tymczasowe uprawnienia.

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

Gdy uprawnienia zostaną przyznane i zrozumiesz różne typy stanu urządzenia, aplikacja będzie mogła obsługiwać przychodzące ładunki FCM.

Obsługa wiadomości

W zależności od bieżącego stanu aplikacji przychodzące ładunki różnych typów wiadomości wymagają różnych implementacji:

Wiadomości na pierwszym planie

Aby obsługiwać wiadomości, gdy aplikacja jest 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, w którym znajdują się szczegółowe informacje o ładunku, takie jak źródło, niepowtarzalny identyfikator, czas wysłania, czy zawierał on powiadomienie itp. Ponieważ wiadomość została pobrana, gdy aplikacja działała na pierwszym planie, możesz bezpośrednio uzyskać dostęp do stanu i kontekstu aplikacji Flutter.

Wiadomości na pierwszym planie i powiadomienia

Wiadomości z powiadomieniami, które nadejdą, gdy aplikacja będzie działać na pierwszym planie, nie będą domyślnie wyświetlać widocznego powiadomienia na Androidzie ani iOS. Możesz jednak zastąpić to działanie:

  • Na Androidzie musisz utworzyć kanał powiadomień o wysokim priorytecie.
  • Na urządzeniach z 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 internetowych.

Platformy Apple i Android

Obsługuj wiadomości w tle, rejestrując moduł obsługi onBackgroundMessage. Po otrzymaniu wiadomości tworzony jest odseparowany proces (tylko na Androidzie, iOS/macOS nie wymagają osobnego odseparowanego procesu), który umożliwia obsługę wiadomości nawet wtedy, gdy aplikacja nie jest uruchomiona.

Oto kilka kwestii, o których warto pamiętać w przypadku funkcji obsługi wiadomości w tle:

  1. Nie może to być funkcja anonimowa.
  2. Musi to być funkcja najwyższego poziomu (np. nie metoda klasy, która wymaga inicjowania).
  3. Jeśli używasz Fluttera w wersji 3.3.0 lub nowszej, obsługę wiadomości musisz oznaczyć adnotacją @pragma('vm:entry-point') bezpośrednio nad deklaracją funkcji (w przeciwnym razie może ona zostać usunięta podczas usuwania nieużywanych funkcji 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 izolowanym środowisku poza kontekstem aplikacji, nie można aktualizować stanu aplikacji ani wykonywać żadnej logiki wpływającej na interfejs. Możesz jednak wykonywać logikę, taką jak żądania HTTP, operacje wejścia/wyjścia (np. aktualizowanie pamięci lokalnej), komunikować się z innymi wtyczkami itp.

Zalecamy też jak najszybsze ukończenie logiki. Wykonywanie długich, wymagających zadań wpływa na wydajność urządzenia i może spowodować zakończenie procesu przez system operacyjny. Jeśli zadania działają dłużej niż 30 sekund, urządzenie może automatycznie zakończyć proces.

Sieć

W internecie napisz skrypt Service Worker w JavaScript, który działa w tle. Użyj service workera do obsługi wiadomości w tle.

Na początek utwórz nowy plik w katalogu web i nadaj mu nazwę firebase-messaging-sw.js:

// Please 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);
});

Plik musi importować pakiety SDK aplikacji i usługi przesyłania wiadomości, inicjować Firebase i udostępniać zmienną messaging.

Następnie należy zarejestrować pracownika. W pliku index.html zarejestruj instancję roboczą, modyfikując tag <script>, który uruchamia Fluttera:

<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ć proces roboczy, modyfikując tag <script>, który uruchamia Fluttera, 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. Proces roboczy zostanie zarejestrowany, a wszystkie wiadomości w tle będą obsługiwane za pomocą tego pliku.

Obsługa interakcji

Powiadomienia są widocznym sygnałem, dlatego użytkownicy często wchodzą z nimi w interakcję (naciskając je). Domyślnym działaniem zarówno na Androidzie, jak i iOS jest otwarcie aplikacji. Jeśli aplikacja jest 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ługiwać interakcję użytkownika po otwarciu aplikacji. Jeśli na przykład nowa wiadomość na czacie zostanie wysłana za pomocą powiadomienia i użytkownik je naciśnie, po otwarciu aplikacji możesz chcieć otworzyć konkretną rozmowę.

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

  • getInitialMessage(): jeśli aplikacja zostanie otwarta po zamknięciu, zwrócony zostanie obiekt Future zawierający obiekt RemoteMessage. Po wykorzystaniu RemoteMessage zostaną usunięte.
  • onMessageOpenedApp: Stream, która wysyła RemoteMessage, gdy aplikacja jest otwierana z tła.

Zalecamy obsługę obu scenariuszy, aby zapewnić użytkownikom płynne działanie aplikacji. Poniższy przykładowy kod 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 to podstawowa ilustracja z użyciem widżetu StatefulWidget.

Lokalizowanie wiadomości

Zlokalizowane ciągi znaków możesz wysyłać na 2 sposoby:

  • Zapisuj na serwerze preferowany język każdego użytkownika i wysyłaj dostosowane powiadomienia w poszczególnych językach.
  • Osadzaj w aplikacji zlokalizowane ciągi znaków i korzystaj z natywnych ustawień językowych systemu operacyjnego.

Oto jak używać drugiej metody:

Android

  1. Określ wiadomości w języku domyślnym w 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 określ wiadomości w języku francuskim w polu 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, messagebody użyj kluczy title_loc_keybody_loc_key w przypadku zlokalizowanej wiadomości i ustaw je na atrybut name wiadomości, którą chcesz wyświetlić.

    Ładunek wiadomości będzie wyglądał tak:

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

iOS

  1. Określ wiadomości w języku domyślnym w 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 określ wiadomości w języku francuskim w polu fr.lproj/Localizable.strings:

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

    Ładunek wiadomości będzie wyglądał tak:

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

Włączanie eksportowania danych o dostarczaniu wiadomości

Dane wiadomości możesz eksportować do BigQuery w celu dalszej analizy. BigQuery umożliwia analizowanie danych przy użyciu BigQuery SQL, eksportowanie ich do innego dostawcy usług w chmurze lub wykorzystywanie ich w modelach ML. Eksport do BigQuery obejmuje wszystkie dostępne dane dotyczące wiadomości, niezależnie od ich typu i sposobu wysyłania (przez interfejs API czy kompozytor powiadomień).

Aby włączyć eksportowanie, najpierw wykonaj 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 zmienić AppDelegate.m na poniższą zawartość.

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

W przypadku internetu musisz zmienić proces roboczy usługi, aby używać pakietu SDK w wersji 9. Wersja 9 musi być spakowana, więc aby skrypt service worker działał, musisz użyć narzędzia do pakowania, np. esbuild. Zobacz przykładową aplikację, aby dowiedzieć się, jak to zrobić.

Po migracji do pakietu 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ć polecenia yarn build, aby wyeksportować nową wersję pliku service worker do folderu web.

Wyświetlanie obrazów w powiadomieniach na urządzeniach z iOS

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

Jeśli używasz uwierzytelniania telefonicznego Firebase, musisz dodać do pliku Podfile pakiet Firebase Auth.

Krok 1. Dodaj rozszerzenie usługi powiadomień

  1. W Xcode kliknij File (Plik) > New (Nowy) > Target (Cel)…
  2. W oknie modalnym pojawi się lista możliwych elementów docelowych. Przewiń ją w dół lub użyj filtra, aby wybrać Rozszerzenie usługi powiadomień. Kliknij Dalej.
  3. Dodaj nazwę produktu (w tym samouczku użyj nazwy „ImageNotification”), wybierz Swift lub Objective-C i kliknij Zakończ.
  4. Aby włączyć schemat, kliknij Aktywuj.

Krok 2. Dodaj cel do pliku Podfile

Swift

Sprawdź, czy nowe rozszerzenie ma dostęp do FirebaseMessagingpakietu SwiftRunner, dodając go do RunnerceluRunner:

  1. W Nawigatorze dodaj pakiet SDK Firebase na platformy Apple: Plik > Dodaj zależności pakietu.

  2. Wyszukaj lub wpisz adres URL pakietu:https://github.com/firebase/firebase-ios-sdk

  3. Dodaj do projektu Runner: Dodaj pakiet

  4. Wybierz FirebaseMessaging i dodaj do celu ImageNotification: Add Package (Dodaj pakiet).

Objective-C

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

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

  2. Przewiń plik do końca 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 pody za pomocą pod install z katalogu ios lub macos.

Krok 3. Użyj narzędzia pomocniczego do rozszerzeń

W tym momencie wszystko powinno działać normalnie. Ostatnim krokiem jest wywołanie narzędzia pomocniczego rozszerzenia.

Swift

  1. W nawigatorze wybierz rozszerzenie ImageNotification.

  2. Otwórz plik NotificationService.swift.

  3. Zastąp zawartość pliku NotificationService.swift tym kodem:

    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. W nawigatorze wybierz rozszerzenie ImageNotification.

  2. Otwórz plik NotificationService.m.

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

    Zastąp zawartość pliku NotificationService.m tym kodem:

    #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
    

Krok 4. Dodaj obraz do ładunku

W ładunku powiadomienia możesz teraz dodać obraz. Zapoznaj się z dokumentacją iOS na temat tworzenia prośby o wysłanie. Pamiętaj, że urządzenie wymusza maksymalny rozmiar obrazu wynoszący 300 KB.