تلقّي الرسائل في تطبيق Flutter

يتم التعامل مع الرسائل الواردة بشكل مختلف حسب حالة الجهاز. لفهم هذه السيناريوهات وكيفية دمج FCM في تطبيقك، من المهم أولاً تحديد الحالات المختلفة التي يمكن أن يكون فيها الجهاز:

الولاية الوصف
العناصر الأمامية عندما يكون التطبيق مفتوحًا ومرئيًا وقيد الاستخدام
الخلفية عندما يكون التطبيق مفتوحًا ولكن في الخلفية (مُصغّرًا) يحدث ذلك عادةً عندما يضغط المستخدم على زر "الشاشة الرئيسية" على الجهاز، أو عندما ينتقل إلى تطبيق آخر باستخدام أداة تبديل التطبيقات، أو عندما يكون التطبيق مفتوحًا في علامة تبويب مختلفة (على الويب).
تم إنهاء عندما يكون الجهاز مقفلًا أو التطبيق غير مفعَّل

هناك بعض الشروط المسبقة التي يجب استيفاؤها قبل أن يتمكّن التطبيق من تلقّي حِزم بيانات الرسائل من خلال إطار عمل إرسال الرسائل (FCM):

  • يجب أن يكون قد تم فتح التطبيق مرة واحدة على الأقل (للسماح بالتسجيل باستخدام "نظام إرسال الرسائل من Google").
  • على نظام التشغيل iOS، إذا مرّر المستخدم التطبيق سريعًا من مبدِّل التطبيقات، يجب إعادة فتحه يدويًا لبدء عمل الرسائل في الخلفية مرة أخرى.
  • على أجهزة Android، إذا أوقف المستخدم التطبيق عن العمل من إعدادات الجهاز، يجب إعادة فتحه يدويًا لبدء عمل الرسائل.
  • على الويب، يجب أن تكون قد طلبت رمزًا مميّزًا (باستخدام getToken()) باستخدام شهادة الإشعارات الفورية على الويب.

طلب إذن لتلقّي الرسائل

على نظام التشغيل iOS وmacOS والويب وAndroid 13 (أو الإصدارات الأحدث)، قبل أن تتمكّن من تلقّي حِزم بيانات FCM على جهازك، عليك أولاً طلب إذن المستخدم.

توفّر حزمة firebase_messaging واجهة برمجة تطبيقات بسيطة لطلب الإذن من خلال طريقة requestPermission. تقبل هذه الواجهة عددًا من الوسيطات المُسمّاة التي تحدِّد نوع الأذونات التي تريد طلبها، مثل ما إذا كان يمكن للرسائل التي تحتوي على بيانات أساسية للإشعارات تشغيل صوت أو قراءة الرسائل من خلال Siri. بشكلٍ تلقائي،تطلب الوسيطة أذونات تلقائية معقولة. توفّر واجهة برمجة التطبيقات المرجعية مستندات كاملة حول الغرض من كل إذن.

للبدء، يمكنك استدعاء الطريقة من تطبيقك (سيتم عرض نافذة مشروطة أصلية على نظام التشغيل iOS، وعلى الويب، سيتم بدء مسار واجهة برمجة التطبيقات الأصلية للمتصفّح):

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. عند استخدام الإصدار 3.3.0 من Flutter أو إصدار أحدث، يجب إضافة تعليق توضيحي إلى معالِج الرسائل باستخدام الرمز @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، وتنفيذ عمليات I/O (مثل تعديل مساحة التخزين المحلية)، والتواصل مع الإضافات الأخرى، وما إلى ذلك.

ننصحك أيضًا بإكمال منطقك في أقرب وقت ممكن. يؤثّر تنفيذ المهام الطويلة والمكثفة في أداء الجهاز ويؤدي إلى إنهاء العملية من قِبل نظام التشغيل. إذا استمر تنفيذ المهام لأكثر من 30 ثانية، قد يوقف الجهاز العملية تلقائيًا.

الويب

على الويب، اكتب مشغّل خدمات JavaScript يتم تشغيله في الخلفية. استخدِم الخدمة العاملة لمعالجة الرسائل في الخلفية.

للبدء، أنشئ ملفًا جديدًا في دليل 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) للتطبيق والمراسلة، وأن يُنشئ 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. في الحمولة على الخادم، استخدِم title_loc_key وbody_loc_key للرسالة المترجَمة بدلاً من المفاتيح title وmessage وbody، واضبطهما على سمة 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 تحليل البيانات باستخدام لغة الاستعلامات البنيوية (SQL) في BigQuery، أو تصديرها إلى مقدّم خدمة آخر لخدمات السحابة الإلكترونية، أو استخدام البيانات في نماذج الذكاء الاصطناعي المخصّصة. يتضمّن التصدير إلى 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 على سبيل المثال لتشغيل مشغّل الخدمة. اطّلِع على مثال التطبيق لمعرفة كيفية تحقيق ذلك.

بعد نقل البيانات إلى الإصدار 9 من حزمة تطوير البرامج (SDK)، يمكنك استخدام الرمز التالي:

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

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

لا تنسَ تشغيل yarn build لتصدير الإصدار الجديد من worker الخدمة إلى مجلد web.

عرض الصور في الإشعارات على أجهزة iOS

على أجهزة Apple، لكي يتمكّن تطبيقك من عرض الصور الواردة من حمولة إطار عمل Firebase للرسائل في إشعارات FCM، عليك إضافة إضافة إضافية لخدمة الإشعارات وضبط تطبيقك لاستخدامها.

إذا كنت تستخدم مصادقة الهاتف في Firebase، عليك إضافة مجموعة Firebase Auth pod إلى Podfile.

الخطوة 1: إضافة إضافة خدمة إشعارات

  1. في Xcode، انقر على ملف > جديد > هدف...
  2. ستعرض نافذة منبثقة قائمة بالأهداف المحتملة، وانتقِل للأسفل أو استخدِم الفلتر لاختيار إضافة خدمة الإشعارات. انقر على التالي.
  3. أضِف اسم منتج (استخدِم "ImageNotification" لمتابعة هذا الدليل التعليمي)، واضبط اللغة على Objective-C، ثم انقر على إنهاء.
  4. فعِّل المخطّط من خلال النقر على تفعيل.

الخطوة 2: إضافة الهدف إلى Podfile

تأكَّد من أنّ الإضافة الجديدة يمكنها الوصول إلى مجموعة 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 أو عدِّلها باستخدام 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 حول كيفية إنشاء طلب إرسال. يُرجى العِلم أنّ الجهاز يفرض الحد الأقصى لحجم الصورة وهو 300 كيلوبايت.