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