معالجة إشعارات FCM في الخلفية والحالة المنتهية
معالجة إشعارات FCM في الخلفية والحالة المنتهية
عندما تصل رسالة FCM (Firebase Cloud Messaging)، قد يكون تطبيق Flutter في أحد ثلاثة حالات دورة حياة: المقدمة (قيد التشغيل ومرئي)، أو الخلفية (مُصغَّر لكنه لا يزال يعمل)، أو المنتهية (مغلق تماماً وليس في الذاكرة). التسليم في المقدمة أمر بسيط — يُطلق FirebaseMessaging.onMessage وتتعامل معه داخل ودجت حي. يكمن التحدي في المعالجة الصحيحة للرسائل عندما يكون التطبيق في الخلفية أو منتهياً، ثم التنقل إلى الشاشة الصحيحة عندما يضغط المستخدم على الإشعار.
firebase_messaging الإصدار 14 وما بعده تغيّر طريقة تسجيل معالج الخلفية المعزول. تحقق دائماً من إصدار الحزمة المثبتة ودليل الترحيل الخاص بها قبل اتباع الدروس القديمة.سيناريوهات التسليم الثلاثة
- المقدمة:
FirebaseMessaging.onMessage.listen(...)— التطبيق مفتوح ونشط. - الخلفية:
FirebaseMessaging.onBackgroundMessage(handler)— العملية حية لكن واجهة المستخدم غير مرئية. يعمل المعالج في معزول Dart منفصل. - المنتهية: يُستخدم نفس معالج
onBackgroundMessage. يبدأ نظام التشغيل معزول Dart جديداً خصيصاً للمعالج؛ لا يبدأ المعزول الرئيسي (واجهة المستخدم). - نقر الإشعار (الخلفية): يُطلق
FirebaseMessaging.onMessageOpenedApp.listen(...)عندما ينقر المستخدم على إشعار بينما التطبيق في الخلفية ويعود إلى المقدمة. - نقر الإشعار (المنتهية): يُرجع
FirebaseMessaging.instance.getInitialMessage()الرسالة التي أطلقت التطبيق من حالة منتهية تماماً.
تسجيل معالج الخلفية
يجب أن يكون معالج رسائل الخلفية دالة من المستوى الأعلى (وليس تابعاً لفئة، ولا دالة مجهولة، ولا إغلاقاً). يجب أيضاً تعليقها بـ @pragma('vm:entry-point') لمنع مترجم Dart AOT من حذفها في إصدارات الإنتاج. سجّلها مع FirebaseMessaging.onBackgroundMessage() قبل استدعاء runApp().
معالج الخلفية من المستوى الأعلى
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
// يجب أن تكون دالة من المستوى الأعلى — وليست داخل فئة أو main()
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// إذا احتجت خدمات Firebase هنا، قم بتهيئتها أولاً:
// await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
print('تم استلام رسالة في الخلفية: ${message.messageId}');
print('عنوان الإشعار: ${message.notification?.title}');
print('حمولة البيانات: ${message.data}');
// نفّذ أعمالاً خفيفة — لا تعرض واجهة مستخدم ولا تستخدم BuildContext هنا.
// المهام الثقيلة يجب تسليمها إلى خدمة خلفية أو WorkManager.
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// سجّل قبل runApp — هذا إلزامي
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(const MyApp());
}
main(). إذا استدعيت مكوّناً إضافياً لم يُهيَّأ في معزول الخلفية، ستحصل على MissingPluginException. استدعِ Firebase.initializeApp() دائماً داخل المعالج إذا احتجت خدمات Firebase أخرى.الاستجابة لنقرات الإشعارات
اكتشاف وصول رسالة هو نصف القصة فقط. التحدي الحقيقي في تطبيقات الإنتاج هو توجيه المستخدم إلى الشاشة الصحيحة عند نقره على إشعار. تحتاج إلى مسارَي كود منفصلَين بحسب ما إذا كان التطبيق في الخلفية أو منتهياً:
onMessageOpenedApp و getInitialMessage
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_setupInteractedMessage();
}
// معالجة نقرات الإشعارات
Future<void> _setupInteractedMessage() async {
// الحالة 1: التطبيق كان منتهياً — أُطلق بنقر على إشعار
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_handleMessageNavigation(initialMessage);
}
// الحالة 2: التطبيق كان في الخلفية — جُلب للمقدمة بنقر على إشعار
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageNavigation);
}
void _handleMessageNavigation(RemoteMessage message) {
final String? screen = message.data['screen'];
if (screen == 'order_details') {
final String? orderId = message.data['order_id'];
Navigator.of(context).pushNamed('/orders/details', arguments: orderId);
} else if (screen == 'chat') {
Navigator.of(context).pushNamed('/chat');
} else {
Navigator.of(context).pushNamed('/notifications');
}
}
@override
Widget build(BuildContext context) => const MaterialApp(home: HomeScreen());
}
getInitialMessage() مرة واحدة فقط، عادةً في initState() لودجت الجذر. الاستدعاء المتعدد آمن — يُرجع دائماً نفس الرسالة حتى إعادة التشغيل التالية — لكنه مهدر. بعد القراءة الأولى الناجحة، احفظ النتيجة في ذاكرة التخزين المؤقت لتجنب تكرار الاستدعاءات غير المتزامنة عند إعادة التشغيل الساخن أثناء التطوير.التوجيه باستخدام NavigatorKey
عند إطلاق getInitialMessage() أو onMessageOpenedApp، قد لا تكون شجرة الودجات مبنية بالكامل بعد، وقد يكون BuildContext غير متاح. الحل الموصى به هو توفير GlobalKey<NavigatorState> لـ MaterialApp والتنقل عبره مباشرةً، متجاوزاً الحاجة إلى BuildContext.
استخدام GlobalKey للتنقل بدون سياق
// تعريف عالمي
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
// آمن للاستدعاء هنا — يُرفق navigatorKey بحلول رسم الإطار الأول،
// و getInitialMessage() غير متزامن.
_setupInteractedMessage();
}
Future<void> _setupInteractedMessage() async {
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) _navigate(initialMessage);
FirebaseMessaging.onMessageOpenedApp.listen(_navigate);
}
void _navigate(RemoteMessage message) {
navigatorKey.currentState?.pushNamed(
'/detail',
arguments: message.data,
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey, // إرفاق المفتاح
home: const HomeScreen(),
);
}
}
الاختلافات بين المنصات: iOS مقابل Android
- على Android، يعمل معالج الخلفية في معزول منفصل حتى عندما يكون التطبيق مفعّلاً جزئياً، ويعرض النظام الإشعار تلقائياً لـرسائل الإشعار. أما رسائل البيانات فقط فيجب معالجتها بالكامل من قِبل معالجك.
- على iOS، يجب طلب إذن المستخدم مع
FirebaseMessaging.instance.requestPermission()وإلا لن تُسلَّم أي إشعارات. يجب أيضاً استدعاءFirebaseMessaging.instance.setForegroundNotificationPresentationOptions()للتحكم في ظهور بانر الإشعار عندما يكون التطبيق في المقدمة. - يُرجع
getInitialMessage()قيمةnullعلى كلا المنصتين إذا فُتح التطبيق بشكل طبيعي (ليس عبر نقر إشعار). تحقق دائماً من عدم النيل قبل الوصول إلى الرسالة.
FirebaseMessaging.onBackgroundMessage() مع معالج من المستوى الأعلى موسوم بـ @pragma لمعالجة الرسائل عندما لا يكون التطبيق في المقدمة. استخدم onMessageOpenedApp لاكتشاف نقرات الإشعارات من حالة الخلفية، وgetInitialMessage() لاكتشاف النقرات من الحالة المنتهية. معاً، تغطي هذه الواجهات البرمجية الثلاث كل سيناريو إطلاق إشعار ممكن.