Как получать сообщения в приложениях Flutter

Выберите платформу: iOS+ Android Веб Flutter Unity C++


Входящие сообщения обрабатываются по-разному в зависимости от состояния устройства. Чтобы понять, как FCM работает в разных ситуациях и как интегрировать его в свое приложение, сначала нужно разобраться, в каких состояниях может находиться устройство.

Штат Описание
Активный режим Когда приложение открыто, видно и используется.
Общая информация Когда приложение открыто, но работает в фоновом режиме (свернуто). Обычно это происходит, когда пользователь нажимает кнопку "Домой" на устройстве, переключается на другое приложение с помощью переключателя приложений или открывает приложение в другой вкладке (в браузере).
Удален Когда устройство заблокировано или приложение не запущено.

Чтобы приложение могло получать полезную нагрузку сообщений с помощью FCM, должны быть выполнены следующие условия:

  • Приложение должно быть открыто хотя бы один раз (чтобы зарегистрироваться в FCM).
  • Если на устройстве iOS пользователь закроет приложение в переключателе приложений, то, чтобы фоновые сообщения снова начали работать, приложение нужно будет открыть вручную.
  • Если пользователь принудительно закроет приложение в настройках устройства Android, то для возобновления работы сообщений потребуется открыть его вручную.
  • На сайте вы должны запросить токен (с помощью getToken()) с сертификатом веб-push.

Как запросить разрешение на получение сообщений

На устройствах с iOS, macOS, Android 13 или более поздней версии, а также в веб-приложениях, прежде чем получать полезные данные FCM, необходимо запросить разрешение у пользователя.

Пакет firebase_messaging предоставляет API для запроса разрешения с помощью метода requestPermission. Этот API принимает ряд именованных аргументов, которые определяют тип разрешений, которые вы хотите запросить, например, может ли обмен сообщениями, содержащий полезную нагрузку уведомлений, вызывать звук или зачитывать сообщения с помощью Siri. По умолчанию метод запрашивает стандартные разрешения. В справочнике по API приведена полная документация о том, для чего нужно каждое разрешение.

Чтобы начать, вызовите метод из приложения (на iOS будет показано встроенное модальное окно, а в браузере будет запущен API):

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

Свойство authorizationStatus объекта NotificationSettings, возвращенное в результате запроса, можно использовать для определения общего решения пользователя:

  • authorized – пользователь предоставил разрешение.
  • denied – пользователь не предоставил разрешение.
  • notDetermined – пользователь ещё не решил, предоставлять ли разрешение.
  • provisional – пользователь предоставил временное разрешение.

Другие свойства в NotificationSettings возвращают информацию о том, включено, отключено или не поддерживается ли определенное разрешение на текущем устройстве.

После того как разрешение будет предоставлено и вы поймете, как работает каждый тип состояния устройства, ваше приложение сможет обрабатывать входящие полезные нагрузки FCM.

Обработка сообщений

В зависимости от текущего состояния приложения входящие полезные нагрузки разных типов сообщений требуют разных способов обработки:

Сообщения на переднем плане

Чтобы обрабатывать сообщения, когда приложение находится на переднем плане, прослушивайте поток 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}');
  }
});

Поток содержит RemoteMessage с подробной информацией о полезной нагрузке, например о том, откуда она поступила, уникальном идентификаторе, времени отправки, наличии уведомления и т. д. Поскольку сообщение было получено, когда приложение находилось на переднем плане, вы можете напрямую получить доступ к состоянию и контексту приложения Flutter.

Сообщения для переднего плана и уведомления

Уведомления, которые приходят, когда приложение открыто, по умолчанию не показываются ни на Android, ни на iOS. Однако это поведение можно переопределить:

  • На устройствах Android необходимо создать канал уведомлений с высоким приоритетом.
  • На устройстве iOS можно изменить параметры презентации для приложения.

Фоновые сообщения

Процесс обработки фоновых сообщений различается на платформах Android, Apple и веб-платформах.

Платформы Apple и Android

Обрабатывайте фоновые сообщения, зарегистрировав обработчик onBackgroundMessage. При получении сообщений создается изолят (только для Android, для iOS/macOS отдельный изолят не требуется), позволяющий обрабатывать сообщения, даже когда приложение не запущено.

При работе с обработчиком фоновых сообщений следует помнить о нескольких вещах:

  1. Она не должна быть анонимной.
  2. Это должна быть функция верхнего уровня (например, не метод класса, требующий инициализации).
  3. Если вы используете Flutter версии 3.3.0 или более поздней, обработчик сообщений должен быть аннотирован с помощью @pragma('vm:entry-point') прямо над объявлением функции (в противном случае он может быть удален во время удаления неиспользуемого кода в режиме выпуска).
@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());
}

Поскольку обработчик выполняется в собственном изолированном процессе вне контекста приложения, он не может обновлять состояние приложения или выполнять логику, влияющую на интерфейс. Однако вы можете выполнять логические операции, такие как HTTP-запросы, операции ввода-вывода (например, обновление локального хранилища), обмен данными с другими плагинами и т. д.

Также рекомендуется как можно скорее завершить логику. Выполнение длительных и ресурсоемких задач влияет на производительность устройства и может привести к тому, что ОС завершит процесс. Если задача выполняется дольше 30 секунд, устройство может автоматически завершить процесс.

Веб-приложение

На сайте создайте сервис-воркер JavaScript, который будет работать в фоновом режиме. Используйте сервис-воркер для обработки фоновых сообщений.

Для начала создайте новый файл в каталоге web и назовите его 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);
});

Файл должен импортировать SDK приложения и обмена сообщениями, инициализировать Firebase и предоставлять переменную messaging.

Затем нужно зарегистрировать сервис-воркер. В файле index.html зарегистрируйте работника, изменив тег <script>, который запускает 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>

Если вы по-прежнему используете старую систему шаблонов, зарегистрировать сервис-воркер можно, изменив тег <script>, который запускает Flutter, следующим образом:

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

Затем перезапустите приложение Flutter. Рабочий процесс будет зарегистрирован, и все фоновые сообщения будут обрабатываться с помощью этого файла.

Обработка взаимодействия

Поскольку уведомления хорошо заметны, пользователи часто взаимодействуют с ними (нажимают на них). По умолчанию на устройствах Android и iOS открывается приложение. Если приложение закрыто, оно будет запущено. Если оно работает в фоновом режиме, то будет выведено на передний план.

В зависимости от содержания уведомления вы можете обрабатывать взаимодействие пользователя при открытии приложения. Например, если новое сообщение чата отправляется с помощью уведомления и пользователь нажимает на него, вы можете открыть определенный разговор при открытии приложения.

Пакет firebase-messaging предоставляет два способа обработки этого взаимодействия:

  • getInitialMessage(): если приложение открывается из закрытого состояния, возвращается Future, содержащий RemoteMessage. После использования RemoteMessage будет удален.
  • onMessageOpenedApp: Stream, который отправляет RemoteMessage, когда приложение открывается из фонового режима.

Рекомендуется обрабатывать оба сценария, чтобы обеспечить удобство работы пользователей. Ниже приведен пример кода, в котором показано, как это сделать:

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

Способ обработки взаимодействия зависит от настроек приложения. В предыдущем примере показана базовая иллюстрация с использованием StatefulWidget.

Локализация сообщений

Локализованные строки можно отправлять двумя способами:

  • Храните на сервере информацию о предпочитаемом языке каждого пользователя и отправляйте уведомления на этом языке.
  • Встройте в приложение локализованные строки и используйте встроенные в операционную систему настройки языка.

Вот как это сделать:

Android

  1. Укажите сообщения на языке по умолчанию в resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Укажите переведенные сообщения в каталоге values-language. Например, укажите сообщения на французском языке в resources/values-fr/strings.xml:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. В полезной нагрузке сервера вместо ключей title, message и body используйте title_loc_key и body_loc_key для локализованного сообщения и задайте для них атрибут name сообщения, которое вы хотите показать.

    Полезная нагрузка сообщения будет выглядеть следующим образом:

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

iOS

  1. Укажите сообщения на языке по умолчанию в Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Укажите переведенные сообщения в каталоге language.lproj. Например, укажите сообщения на французском языке в fr.lproj/Localizable.strings:

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

    Полезная нагрузка сообщения будет выглядеть следующим образом:

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

Как включить экспорт данных о доставке сообщений

Вы можете экспортировать данные о сообщениях в BigQuery для дальнейшего анализа. BigQuery позволяет анализировать данные с помощью SQL BigQuery, экспортировать их в другое облачное хранилище или использовать для собственных моделей машинного обучения. Экспорт в BigQuery включает все доступные данные о сообщениях, независимо от их типа и от того, отправлены ли они с помощью API или Компоновщика уведомлений.

Чтобы включить экспорт, выполните инструкции из статьи Экспорт данных в BigQuery. Если включить его на уровне экземпляра приложения, вы сможете запрашивать у конечных пользователей разрешение на анализ данных о доставке сообщений (рекомендуется). Чтобы включить экспорт программным способом, выполните следующие инструкции:

Android

Вы можете использовать следующий код:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

В iOS нужно заменить AppDelegate.m следующим кодом:

#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

Веб-приложение

Чтобы использовать версию 9 SDK на сайте, вам нужно изменить сервис-воркер. Версию 9 необходимо упаковать, поэтому для работы сервисного работника вам нужно использовать упаковщик, например esbuild. Посмотрите, как это реализовано в примере приложения.

После перехода на SDK версии 9 вы можете использовать следующий код:

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

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

Не забудьте запустить yarn build, чтобы экспортировать новую версию сервисного работника в папку web.

Как показывать изображения в уведомлениях на iOS

Чтобы на устройствах Apple входящие уведомления FCM показывали изображения из полезной нагрузки FCM, необходимо добавить дополнительное расширение службы уведомлений и настроить приложение на его использование.

Если вы используете аутентификацию по номеру телефона Firebase, добавьте в Podfile модуль Firebase Auth.

Шаг 1. Добавьте расширение сервиса уведомлений

  1. В Xcode нажмите File (Файл) > New (Создать) > Target (Цель).
  2. В модальном окне появится список возможных целей. Прокрутите его или используйте фильтр, чтобы выбрать Расширение сервиса уведомлений. Нажмите Далее.
  3. Добавьте название товара (в этом руководстве используется ImageNotification), выберите Swift или Objective-C и нажмите Finish (Готово).
  4. Включите схему, нажав Активировать.

Шаг 2. Добавьте цель в Podfile

Swift

Убедитесь, что у нового расширения есть доступ к пакету FirebaseMessaging swift для этого добавьте его в целевой объект Runner:

  1. В навигаторе добавьте Firebase SDK для платформ Apple: File (Файл) > Add Package Dependencies (Добавить зависимости пакетов).

  2. Выполните поиск или введите URL пакета: none https://github.com/firebase/firebase-ios-sdk

  3. Добавить в проект Runner: Добавить пакет

  4. Выберите FirebaseMessaging и добавьте в целевой объект ImageNotification: Add Package (Добавить пакет).

Objective-C

Убедитесь, что у нового расширения есть доступ к pod-файлу Firebase/Messaging, добавив его в Podfile:

  1. В навигаторе откройте Podfile: Pods > Podfile.

  2. Перейдите в конец файла и добавьте:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Установите или обновите свои модули, используя pod install из каталога ios или macos.

Шаг 3. Используйте помощника по расширениям

На этом этапе все должно работать в обычном режиме. Последний шаг – вызов помощника по расширениям.

Swift

  1. В навигаторе выберите расширение ImageNotification.

  2. Откройте файл NotificationService.swift.

  3. Замените содержимое файла NotificationService.swift следующим кодом:

    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. В навигаторе выберите расширение ImageNotification.

  2. Откройте файл NotificationService.m.

  3. В верхней части файла импортируйте FirebaseMessaging.h сразу после NotificationService.h.

    Замените содержимое файла NotificationService.m следующим кодом:

    #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
    

Шаг 4. Добавьте изображение в полезную нагрузку

Теперь в полезную нагрузку уведомления можно добавить изображение. Подробнее о том, как создать запрос на отправку…