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 metoda żąda rozsądnych uprawnień domyślnych. 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 za pomocą BigQuery SQL, eksportowanie ich do innego dostawcy usług w chmurze lub używanie 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.