Odbieranie wiadomości w aplikacjach Flutter

Wybierz platformę: iOS+ Android Web Flutter Unity C++


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 poznać różne stany, w jakich może znajdować się urządzenie:

Stan Opis
Pierwszy plan Gdy aplikacja jest otwarta, widoczna i używana.
Tło 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ńczona Gdy urządzenie jest zablokowane lub aplikacja nie jest uruchomiona.

Aby aplikacja mogła odbierać ładunki wiadomości za pomocą FCM, musi spełniać kilka warunków wstępnych:

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

Prośba o zgodę na odbieranie wiadomości

W iOS, macOS, internecie i Androidzie 13 (lub nowszym) przed odebraniem ładunków FCM na urządzeniu musisz najpierw poprosić o zgodę użytkownika.

Pakiet firebase_messaging udostępnia interfejs API do wysyłania prośby o zgodę za pomocą metody requestPermission. Ten interfejs API akceptuje kilka nazwanych argumentów, które określają typ uprawnień, o które chcesz poprosić, np. czy przesyłanie wiadomości zawierające ładunki powiadomień może wywoływać dźwięk lub odczytywać wiadomości za pomocą Siri. Domyślnie metoda prosi o rozsądne uprawnienia domyślne. Dokumentacja interfejsu API zawiera pełne informacje o tym, do czego służy każde uprawnienie.

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

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

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

Gdy uprawnienia zostaną przyznane i zrozumiesz różne typy stanów urządzenia, aplikacja może zacząć 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 z różnymi informacjami o ładunku, takimi jak źródło, unikalny identyfikator, czas wysłania, czy zawierał powiadomienie itp. Ponieważ wiadomość została pobrana, gdy aplikacja jest 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 docierają, gdy aplikacja jest na pierwszym planie, domyślnie nie wyświetlają widocznego powiadomienia ani na Androidzie, ani w iOS. Można jednak zastąpić to działanie:

  • W 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ę w zależności od platformy (Android, Apple i internet).

Platformy Apple i Android

Obsługuj wiadomości w tle, rejestrując obsługę onBackgroundMessage. Gdy wiadomości są odbierane, tworzony jest izolowany proces (tylko w Androidzie, iOS/macOS nie wymaga osobnego izolowanego procesu), co umożliwia obsługę wiadomości nawet wtedy, gdy aplikacja nie jest uruchomiona.

Oto kilka kwestii, o których warto pamiętać w przypadku 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 należy oznaczyć adnotacją @pragma('vm:entry-point') bezpośrednio nad deklaracją funkcji (w przeciwnym razie może zostać usunięta podczas usuwania nieużywanego kodu 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ż obsługa działa w osobnym izolowanym procesie 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 wykonanie logiki. Długotrwałe, 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 zakończyć proces.

Sieć

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

Aby rozpocząć, utwórz nowy plik w katalogu web i nazwij go 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);
});

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

Następnie należy zarejestrować skrypt. W pliku index.html zarejestruj skrypt, 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ć skrypt, 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. Skrypt 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ę (klikając je). Domyślnie zarówno w Androidzie, jak i w iOS otwiera się aplikacja. 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, a użytkownik je kliknie, możesz chcieć otworzyć konkretną rozmowę po otwarciu aplikacji.

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

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

Zalecamy obsługę obu scenariuszy, aby zapewnić użytkownikom płynną obsługę. Poniższy przykład kodu 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 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("...");
  }
}

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

Lokalizowanie wiadomości

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

  • Przechowuj preferowany język każdego użytkownika na serwerze i wysyłaj dostosowane powiadomienia w każdym języku.
  • Osadź zlokalizowane ciągi znaków w aplikacji i korzystaj z wbudowanych ustawień regionalnych systemu operacyjnego.

Oto jak używać drugiej metody:

Android

  1. Określ wiadomości w języku domyślnym 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 określ wiadomości w języku francuskim w pliku 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 dla 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 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 określ wiadomości w języku francuskim w pliku 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 eksportu danych dostarczania wiadomości

Aby przeprowadzić dalszą analizę, możesz wyeksportować dane wiadomości do BigQuery. BigQuery umożliwia analizowanie danych za pomocą BigQuery SQL, eksportowanie ich do innego dostawcy usług w chmurze lub używanie ich w niestandardowych modelach ML. Eksport do BigQuery obejmuje wszystkie dostępne dane wiadomości, niezależnie od typu wiadomości i tego, czy wiadomość została wysłana za pomocą interfejsu API czy kompozytora powiadomień.

Aby włączyć eksport, najpierw wykonaj czynności opisane w dokumencie Eksportowanie danych do BigQuery. Programowe włączenie eksportu na poziomie instancji aplikacji umożliwia poproszenie użytkowników o zgodę na analizowanie danych dostarczania wiadomości (zalecane). Aby programowo włączyć eksport:

Android

Możesz użyć tego kodu:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

W przypadku iOS musisz zmienić plik AppDelegate.m na tę 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ć skrypt Service Worker, aby używać pakietu SDK w wersji 9. Wersja 9 musi być spakowana, dlatego do działania skryptu Service Worker musisz użyć narzędzia do pakowania, takiego jak 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ę skryptu Service Worker do folderu web.

Wyświetlanie obrazów w powiadomieniach w iOS

Aby przychodzące powiadomienia FCM wyświetlały obrazy z ładunku FCM na urządzeniach Apple, 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 > New > Target... (Plik > Nowy > Cel).
  2. Wyświetli się modal z listą możliwych celów. Przewiń lub użyj filtra, aby wybrać Notification Service Extension (Rozszerzenie usługi powiadomień). Kliknij Next (Dalej).
  3. Dodaj nazwę produktu (aby postępować zgodnie z tym samouczkiem, użyj nazwy „ImageNotification”), wybierz Swift lub Objective-C i kliknij Finish (Zakończ).
  4. Włącz schemat, klikając Activate (Aktywuj).

Krok 2. Dodaj cel do pliku Podfile

Swift

Upewnij się, że nowe rozszerzenie ma dostęp do pakietu Swift FirebaseMessaging, dodając go do celu Runner:

  1. W Navigatorze, dodaj pakiet SDK Firebase dla platform Apple: File > Add Package Dependencies...

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

  3. Dodaj do projektu Runner: Add Package (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 pakietu Firebase/Messaging, dodając go do pliku Podfile:

  1. W Navigatorze otwórz plik Podfile: Pods > Podfile (Pakiety > Podfile).

  2. Przejdź na dół 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 pakiety za pomocą polecenia pod install w katalogu ios lub macos.

Krok 3. Użyj narzędzia pomocniczego rozszerzenia

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

Swift

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

  2. Otwórz plik NotificationService.m.

  3. U góry pliku zaimportuj FirebaseMessaging.h bezpośrednio po NotificationService.h.

    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. Więcej informacji znajdziesz w artykule o tworzeniu żądania wysłania.