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

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

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

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

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

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

ב-iOS, ב-macOS, בדפדפן וב-Android 13 (ואילך), לפני שאפשר יהיה להשתמש במטענים ייעודיים (payloads) של FCM שהתקבלו במכשיר שלך, עליך לבקש תחילה את הרשאת המשתמש.

החבילה firebase_messaging מספקת ממשק API פשוט לבקשת הרשאה באמצעות השיטה requestPermission. ה-API הזה מקבל מספר ארגומנטים בעלי שם שמגדירים את סוג ההרשאות שרוצים לבקש. למשל, הודעות שמכילות מטענים ייעודיים של התראות יכולות להשמיע צליל או להקריא הודעות דרך Siri. כברירת מחדל, ה-method מבקש הרשאות ברירת מחדל הגיוניות. ב-Reference 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 חוזרים גם אם הרשאה ספציפית מופעלת, מושבתת או לא נתמכת במכשיר.

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

טיפול בהודעות

בהתאם למצב הנוכחי של האפליקציה, מטענים ייעודיים (payloads) נכנסים של סוגי הודעות יש צורך בהטמעות שונות כדי לטפל בהן:

הודעות שפועלות בחזית

כדי לטפל בהודעות בזמן שהאפליקציה פועלת בחזית, צריך להאזין לשידור של 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, המפרט פרטים שונים על המטען הייעודי (Payload), כמו המקור שלו, המזהה הייחודי, זמן השליחה, אם הוא הכיל התראה ועוד. ההודעה התקבלה בזמן שהאפליקציה פועלת בחזית, ולכן יש לך אפשרות לגשת ישירות ל-Flutter במצב ובהקשר של האפליקציה.

הודעות בחזית והתראות

הודעות שמגיעות בזמן שהאפליקציה פועלת בחזית לא יוצגו כברירת מחדל, גם ב-Android וגם ב-iOS. עם זאת, אפשר לשנות את ההתנהגות הזו:

  • ב-Android, צריך ליצור רמת 'עדיפות גבוהה' ערוץ התראות.
  • ב-iOS, אפשר לעדכן את אפשרויות ההצגה של האפליקציה.

הודעות ברקע

תהליך הטיפול בהודעות רקע שונה בממשק המקורי (Android Apple) ופלטפורמות מבוססות-אינטרנט.

פלטפורמות של Apple ו-Android

טיפול בהודעות ברקע על ידי רישום handler של onBackgroundMessage. כאשר מתקבלות הודעות, נוצר בידוד (Android בלבד, ב-iOS או ב-macOS לא נדרש בידוד נפרד) כדי שתוכלו לטפל בהודעות גם כשהאפליקציה לא פועלת.

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

  1. אסור שהיא תהיה פונקציה אנונימית.
  2. זו חייבת להיות פונקציה ברמה העליונה (למשל, לא שיטת מחלקה שדורשת אתחול).
  3. כשמשתמשים ב-Flutter בגרסה 3.3.0 ואילך, צריך להוסיף ל-handler של ההודעה הערות עם @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());
}

מכיוון שה-handler פועל בבידוד עצמי מחוץ להקשר של האפליקציות, לא ניתן לעדכן במצב האפליקציה או מפעילים לוגיקה של ממשק משתמש שמשפיעה. עם זאת, אפשר לבצע פעולות לוגיות כמו בקשות 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 להעברת הודעות, להפעיל את 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.

התאמת ההודעות לשוק המקומי

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

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

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

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. במטען הייעודי (Payload) של השרת, במקום להשתמש במפתחות 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) כדי להשתמש בגרסת v9 של ה-SDK. כדי להשתמש ב-Service Worker, צריך לארוז את הגרסה v9. לשם כך, צריך להשתמש ב-bundler כמו esbuild, למשל. אפשר לעיין באפליקציה לדוגמה כדי לראות איך לעשות זאת.

לאחר המעבר ל-v9 SDK, תוכלו להשתמש בקוד הבא:

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

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

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

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

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

אם משתמשים באימות של מספר טלפון ב-Firebase, צריך להוסיף את רצף המודעות של Firebase Auth ל-Podfile.

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

  1. ב-Xcode, לוחצים על File > (קובץ >) חדש > יעד...
  2. חלון יציג רשימה של יעדים אפשריים. גוללים למטה או משתמשים במסנן כדי לבחור באפשרות Notification Service Extension. לוחצים על Next.
  3. מוסיפים שם מוצר (אפשר להשתמש ב-ImageNotification כדי לפעול לפי המדריך), מגדירים את השפה כ-Objective-C ולוחצים על Finish.
  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. מתקינים או מעדכנים את קבוצות ה-Pod באמצעות 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 – מוסיפים את התמונה למטען הייעודי (Payload)

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