קבלת הודעות באפליקציה של Flutter

הטיפול בהודעות נכנסות משתנה בהתאם למצב המכשיר. כדי להבין את התרחישים האלה ואת האופן שבו משלבים את FCM באפליקציה שלכם, חשוב קודם להבין את המצבים השונים שבהם מכשיר יכול להיות:

מדינה תיאור
חזית כשהאפליקציה פתוחה, מוצגת ומשמשת.
רקע כשהאפליקציה פתוחה, אבל ברקע (מוזערת). המצב הזה מתרחש בדרך כלל כשהמשתמש לחץ על לחצן הבית במכשיר, עבר לאפליקציה אחרת באמצעות מחליף האפליקציות או שהאפליקציה פתוחה בכרטיסייה אחרת (באינטרנט).
Terminated כשהמכשיר נעול או שהאפליקציה לא פועלת.

יש כמה תנאים מוקדמים שצריך לעמוד בהם כדי שהאפליקציה תוכל לקבל עומסי הודעות דרך FCM:

  • האפליקציה צריכה להיפתח לפחות פעם אחת (כדי לאפשר רישום ב-FCM).
  • ב-iOS, אם המשתמש מחליק את האפליקציה מבורר האפליקציות, צריך לפתוח אותה מחדש באופן ידני כדי שהודעות ברקע יתחילו לפעול שוב.
  • ב-Android, אם המשתמש סוגר את האפליקציה בכוח דרך הגדרות המכשיר, צריך לפתוח אותה מחדש באופן ידני כדי שההודעות יתחילו לפעול.
  • באינטרנט, צריך לבקש אסימון (באמצעות getToken()) עם אישור הדחיפה לאינטרנט.

שליחת בקשה לקבלת הודעות

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

מכיוון שהמתקן פועל ב-isolate משלו מחוץ להקשר של האפליקציות, אי אפשר לעדכן את מצב האפליקציה או להריץ ממשק משתמש שמשפיע על הלוגיקה. עם זאת, אפשר לבצע פעולות לוגיות כמו בקשות HTTP, לבצע פעולות IO (למשל עדכון האחסון המקומי), לתקשר עם יישומי פלאגין אחרים וכו'.

מומלץ גם להשלים את הלוגיקה בהקדם האפשרי. הרצת משימות ארוכות ואינטנסיביות משפיעה על ביצועי המכשיר ועלולה לגרום למערכת ההפעלה לסיים את התהליך. אם משימות יפעלו במשך יותר מ-30 שניות, יכול להיות שהמכשיר יסיים את התהליך באופן אוטומטי.

אינטרנט

באינטרנט, כותבים Service Worker ב-JavaScript שפועל ברקע. שימוש ב-service worker לטיפול בהודעות ברקע.

כדי להתחיל, יוצרים קובץ חדש בספרייה web ומכנים אותו firebase-messaging-sw.js:

// Please see this file for the latest firebase-js-sdk version:
// https://github.com/firebase/flutterfire/blob/master/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js");

firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
});

const messaging = firebase.messaging();

// Optional:
messaging.onBackgroundMessage((message) => {
  console.log("onBackgroundMessage", message);
});

הקובץ צריך לייבא גם את ערכות ה-SDK של האפליקציה וגם את ערכות ה-SDK של Messaging, לאתחל את 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 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("...");
  }
}

אופן הטיפול באינטראקציה תלוי בהגדרת האפליקציה. בדוגמה שלמעלה מוצג איור בסיסי באמצעות StatefulWidget.

התאמה לשוק המקומי ב-Messages

יש שתי דרכים לשלוח מחרוזות מותאמות לשוק המקומי:

  • לשמור בשרת את השפה המועדפת של כל אחד מהמשתמשים ולשלוח התראות בהתאמה אישית לכל שפה
  • הטמעת מחרוזות מותאמות לשוק המקומי באפליקציה ושימוש בהגדרות הלוקאל המקומי של מערכת ההפעלה

כך משתמשים בשיטה השנייה:

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 של ההודעה שרוצים להציג.

    עומס העבודה של ההודעה ייראה כך:

    {
      "data": {
        "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";
    

    עומס העבודה של ההודעה ייראה כך:

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

הפעלת ייצוא של נתוני שליחת הודעות

אפשר לייצא את נתוני ההודעות ל-BigQuery לצורך ניתוח נוסף. BigQuery מאפשר לנתח את הנתונים באמצעות BigQuery SQL, לייצא אותם לספק ענן אחר או להשתמש בהם למודלים מותאמים אישית של למידת מכונה. ייצוא ל-BigQuery כולל את כל הנתונים הזמינים של ההתראות, ללא קשר לסוג ההתראה או לדרך שבה ההתראה נשלחה – דרך ה-API או דרך הכלי ליצירת התראות.

כדי להפעיל את הייצוא, פועלים לפי השלבים המפורטים כאן, ואז פועלים לפי ההוראות הבאות:

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

אינטרנט

באינטרנט, צריך לשנות את ה-service worker כדי להשתמש בגרסה 9 של ה-SDK. כדי להשתמש ב-Service Worker, צריך לארוז את הגרסה v9. לשם כך, צריך להשתמש ב-bundler כמו esbuild, למשל. באפליקציית הדוגמה מוסבר איך עושים את זה.

אחרי שעוברים ל-SDK בגרסה 9, אפשר להשתמש בקוד הבא:

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

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

אל תשכחו להריץ את הפקודה yarn build כדי לייצא את הגרסה החדשה של ה-service worker לתיקייה web.

הצגת תמונות בהתראות ב-iOS

במכשירי Apple, כדי שהתראות FCM נכנסות יוכלו להציג תמונות ממטען הייעודי של FCM, צריך להוסיף תוסף נוסף של שירות התראות ולהגדיר את האפליקציה כך שתשתמש בו.

אם אתם משתמשים באימות טלפון ב-Firebase, עליכם להוסיף את ה-pod של Firebase Auth ל-Podfile.

שלב 1 – מוסיפים תוסף של שירות התראות

  1. ב-Xcode, לוחצים על File (קובץ) > New (חדש) > Target (יעד)…
  2. תופיע תיבת דו-שיח עם רשימה של יעדים אפשריים. גוללים למטה או משתמשים במסנן כדי לבחור באפשרות Notification Service Extension. לוחצים על Next.
  3. מוסיפים שם למוצר (כדי לעקוב אחרי המדריך הזה, אפשר להשתמש בשם 'ImageNotification'), מגדירים את השפה כ-Objective-C ולוחצים על סיום.
  4. מפעילים את התוכנית בלחיצה על הפעלה.

שלב 2 – מוסיפים יעד ל-Podfile

מוודאים שיש לסיומת החדשה גישה ל-pod‏ Firebase/Messaging על ידי הוספה שלה ל-Podfile:

  1. פותחים את Podfile ב-Navigator: 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. מתקינים או מעדכנים את ה-pods באמצעות pod install מהספרייה ios או macos.

שלב 3 – שימוש בכלי העזר של התוסף

בשלב הזה, הכול אמור לפעול כרגיל. השלב האחרון הוא להפעיל את הכלי לעזרה בתוסף.

  1. בחלונית הניווט, בוחרים את התוסף ImageNotification.

  2. פותחים את הקובץ NotificationService.m.

  3. בחלק העליון של הקובץ, מייבאים את FirebaseMessaging.h מיד אחרי NotificationService.h, כפי שמוצג בהמשך.

    מחליפים את התוכן של NotificationService.m ב:

    #import "NotificationService.h"
    #import "FirebaseMessaging.h"
    #import "FirebaseAuth.h" // Add this line if you are using FirebaseAuth phone authentication
    #import <UIKit/UIKit.h> // Add this line if you are using FirebaseAuth phone authentication
    
    @interface NotificationService ()
    
    @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
    @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
    
    @end
    
    @implementation NotificationService
    
    /* Uncomment this if you are using Firebase Auth
    - (BOOL)application:(UIApplication *)app
                openURL:(NSURL *)url
                options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
      if ([[FIRAuth auth] canHandleURL:url]) {
        return YES;
      }
      return NO;
    }
    
    - (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
      for (UIOpenURLContext *urlContext in URLContexts) {
        [FIRAuth.auth canHandleURL:urlContext.URL];
      }
    }
    */
    
    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
    
        // Modify the notification content here...
        [[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler];
    }
    
    - (void)serviceExtensionTimeWillExpire {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        self.contentHandler(self.bestAttemptContent);
    }
    
    @end
    

שלב 4 – מוסיפים את התמונה לעומס התועלת

עכשיו אפשר להוסיף תמונה למטען הייעודי של ההתראה. במסמכי התיעוד של iOS מוסבר איך יוצרים בקשת שליחה. חשוב לזכור שהמכשיר אוכף גודל תמונה מקסימלי של 300KB.