الإشعارات الفورية والخدمات الخلفية

معالجة إشعارات FCM في المقدمة

16 دقيقة الدرس 2 من 11

معالجة إشعارات FCM في المقدمة

عندما يكون تطبيق Flutter مفتوحاً وفي المقدمة، تُسلِّم Firebase Cloud Messaging (FCM) الرسائل بصمت — لا يعرض نظام التشغيل أي لافتة إشعار. بدلاً من ذلك، يستقبل التطبيق الرسالة عبر دفق FirebaseMessaging.onMessage، ويقع على عاتقك الكامل تحديد طريقة عرض هذه المعلومات للمستخدم. يتناول هذا الدرس كيفية الاستماع للرسائل الواردة وتحليل حمولة RemoteMessage وعرض تنبيه داخل التطبيق.

ملاحظة: تميّز FCM بين ثلاث حالات للتطبيق — المقدمة، والخلفية، والإنهاء. لا يُطلَق دفق onMessage إلا عندما يكون التطبيق في المقدمة فقط. تستخدم حالات الخلفية والإنهاء معالجات مختلفة ستُغطَّى في دروس لاحقة.

إعداد دفق onMessage

تُعيد الخاصية FirebaseMessaging.onMessage دفق Stream<RemoteMessage>. تشترك فيه داخل initState لودجت طويل العمر (عادةً الودجت الجذري) وتُلغي الاشتراك في dispose لتجنب تسرب الذاكرة.

الاشتراك في onMessage داخل initState

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';

class AppRoot extends StatefulWidget {
  const AppRoot({super.key});

  @override
  State<AppRoot> createState() => _AppRootState();
}

class _AppRootState extends State<AppRoot> {
  // الاحتفاظ باشتراك الدفق لإلغائه لاحقاً
  late final StreamSubscription<RemoteMessage> _messageSubscription;

  @override
  void initState() {
    super.initState();
    // الاستماع لرسائل FCM في المقدمة
    _messageSubscription = FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
  }

  void _handleForegroundMessage(RemoteMessage message) {
    // يُستدعى على الخيط الرئيسي عند وصول رسالة في المقدمة
    debugPrint('تم استلام رسالة في المقدمة: ${message.messageId}');
    _showInAppAlert(message);
  }

  @override
  void dispose() {
    _messageSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: HomeScreen());
  }
}

تحليل حمولة RemoteMessage

يحتوي كائن RemoteMessage على عدة حقول مهمة يجب معرفة كيفية الوصول إليها:

  • message.notification — كائن RemoteNotification? يحتوي على سلسلتَي title وbody. يكون موجوداً فقط عندما يرسل الخادم حمولة إشعار.
  • message.data — خريطة Map<String, String> تحتوي على أزواج مفتاح-قيمة مخصصة يُحددها الواجهة الخلفية. موجودة دائماً (قد تكون فارغة).
  • message.messageId — معرّف فريد لإزالة التكرار.
  • message.sentTime — التاريخ DateTime? الذي أرسل فيه الخادم الرسالة.
  • message.from — المرسِل (موضوع أو رمز تسجيل FCM).
نصيحة: تحقق دائماً من قيمة null في message.notification قبل الوصول إلى حقوله. الرسائل النقية التي تحتوي على بيانات فقط (المرسَلة بدون كتلة notification) ستكون قيمة notification فيها null، لكن خريطة data ستكون ممتلئة.

تحليل حقول الإشعار والبيانات

void _handleForegroundMessage(RemoteMessage message) {
  // استخراج حقول الإشعار بأمان
  final String title = message.notification?.title ?? 'إشعار جديد';
  final String body  = message.notification?.body  ?? '';

  // استخراج البيانات المخصصة المرسَلة من الواجهة الخلفية
  final String? type     = message.data['type'];
  final String? targetId = message.data['target_id'];

  debugPrint('العنوان: $title');
  debugPrint('النص: $body');
  debugPrint('النوع: $type، المعرف المستهدف: $targetId');

  // التوجيه بناءً على نوع البيانات المخصصة
  if (type == 'chat') {
    _showChatAlert(title, body, targetId);
  } else if (type == 'promo') {
    _showPromoAlert(title, body);
  } else {
    _showGenericAlert(title, body);
  }
}

عرض تنبيه داخل التطبيق باستخدام SnackBar

أبسط طريقة لعرض إشعار في المقدمة هي استخدام SnackBar. إنه غير مُزعج وألِيف للمستخدمين وسهل التطبيق. تحتاج إلى BuildContext صالح مع سلف من نوع Scaffold، أو استخدام ScaffoldMessengerKey عالمي للوصول من خارج شجرة الودجات.

عرض SnackBar لرسائل المقدمة

// أعلن عن مفتاح عالمي على المستوى الأعلى (مثلاً في main.dart)
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
    GlobalKey<ScaffoldMessengerState>();

// مرّره إلى MaterialApp
MaterialApp(
  scaffoldMessengerKey: scaffoldMessengerKey,
  home: const HomeScreen(),
);

// ثم استدعِه من أي مكان، بما في ذلك خارج شجرة الودجات:
void _showInAppAlert(RemoteMessage message) {
  final String title = message.notification?.title ?? 'إشعار';
  final String body  = message.notification?.body  ?? '';

  scaffoldMessengerKey.currentState?.showSnackBar(
    SnackBar(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
          if (body.isNotEmpty) Text(body),
        ],
      ),
      duration: const Duration(seconds: 4),
      behavior: SnackBarBehavior.floating,
      action: SnackBarAction(
        label: 'عرض',
        onPressed: () => _handleNotificationTap(message),
      ),
    ),
  );
}

عرض تنبيه في نافذة حوار بدلاً من ذلك

للرسائل ذات الأولوية العالية قد تُفضّل استخدام نافذة حوار مشروطة. استخدم نفس نمط المفتاح العالمي للحصول على سياق التراكب، أو استدعِ showDialog من داخل الودجت إذا كان لديك سياق محلي.

نافذة حوار مشروطة للإشعارات الحرجة في المقدمة

void _showCriticalAlert(BuildContext context, RemoteMessage message) {
  showDialog<void>(
    context: context,
    barrierDismissible: false,
    builder: (_) => AlertDialog(
      title: Text(message.notification?.title ?? 'تنبيه'),
      content: Text(message.notification?.body ?? ''),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('إغلاق'),
        ),
        ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop();
            _handleNotificationTap(message);
          },
          child: const Text('فتح'),
        ),
      ],
    ),
  );
}

أفضل الممارسات لمعالجة المقدمة

  • ألغِ دائماً StreamSubscription في dispose لتجنب معالجة الرسائل بعد إزالة الودجت.
  • استخدم ScaffoldMessengerKey عالمياً أو مفتاح متصفح نافذة حتى يتمكن المعالج من عرض واجهة المستخدم بغض النظر عن الشاشة النشطة حالياً.
  • ميّز بناءً على حقل data['type'] لتوجيه فئات الإشعارات المختلفة (الدردشة، تحديث الطلب، العرض الترويجي) إلى الشاشة الصحيحة.
  • تجنب استدعاء setState مباشرةً داخل معالج الرسائل ما لم تكن متأكداً من أن الودجت لا يزال مُثبَّتاً — احمِ دائماً بـ if (mounted).
  • قم بإزالة التكرار باستخدام message.messageId إذا كان خادمك قد يُرسل تكرارات.
تحذير: لا تعتمد على message.notification للمنطق التجاري. على نظام Android، قد تُدمج FCM حقل الإشعار في ظروف معينة عندما يكون التطبيق في المقدمة. قم دائماً بترميز البيانات القابلة للتنفيذ في message.data وعامل حقول الإشعار كتلميحات عرض فقط.

ملخص

لمعالجة رسائل FCM أثناء وجود التطبيق في المقدمة: استمع إلى FirebaseMessaging.onMessage في initState، وألغِ الاشتراك في dispose، وحلِّل message.notification بأمان للحصول على نص العرض وmessage.data لمنطق التوجيه، ثم اعرض التنبيه عبر SnackBar (للأولوية المنخفضة) أو Dialog (للأولوية العالية) باستخدام ScaffoldMessengerKey عالمي حتى تتمكن أي شاشة من عرض الإشعار بغض النظر عن حالة التنقل الحالية.