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

الإشعارات المحلية المجدولة والدورية

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

الإشعارات المحلية المجدولة والدورية

إطلاق إشعار فوري أمر بسيط، لكن التطبيقات الحقيقية غالباً ما تحتاج إلى جدولة إشعار ليُطلَق في تاريخ ووقت محدد في المستقبل، أو إعداد إشعار متكرر يُطلَق كل يوم أو كل أسبوع أو على فترات مخصصة. تعرض حزمة flutter_local_notifications واجهة برمجية خاصة بالجدولة المنطقية للوقت (zoned scheduling API) تتعامل مع المناطق الزمنية بشكل صحيح، متجنبةً الأخطاء الكلاسيكية المتعلقة بالتوقيت الصيفي وإزاحات UTC.

ملاحظة: تتطلب الجدولة المنطقية للوقت حزمة timezone بجانب flutter_local_notifications. استدعِ دائماً tz.initializeTimeZones() عند بدء التطبيق وعيّن المنطقة الزمنية المحلية بـ tz.setLocalLocation() قبل جدولة أي إشعار.

إعداد حزمة timezone

أضف كلتا الحزمتين إلى pubspec.yaml، ثم هيّئ قاعدة بيانات المناطق الزمنية في دالة main() قبل runApp():

تهيئة قاعدة بيانات المناطق الزمنية

import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 1. تحميل قاعدة بيانات المناطق الزمنية الكاملة
  tz.initializeTimeZones();

  // 2. تعيين المنطقة الزمنية المحلية للجهاز (تُقرأ من نظام التشغيل)
  final String currentTimeZone =
      await FlutterTimezone.getLocalTimezone(); // حزمة flutter_timezone
  tz.setLocalLocation(tz.getLocation(currentTimeZone));

  // 3. تهيئة flutter_local_notifications (Android + iOS)
  const AndroidInitializationSettings androidSettings =
      AndroidInitializationSettings('@mipmap/ic_launcher');
  const DarwinInitializationSettings iosSettings =
      DarwinInitializationSettings();
  const InitializationSettings initSettings = InitializationSettings(
    android: androidSettings,
    iOS: iosSettings,
  );
  await flutterLocalNotificationsPlugin.initialize(initSettings);

  runApp(const MyApp());
}

جدولة إشعار لمرة واحدة

استخدم zonedSchedule() لإطلاق إشعار في تاريخ ووقت محدد في المنطقة الزمنية المحلية للمستخدم. تأخذ الدالة TZDateTime — وهو DateTime يعي المنطقة الزمنية — لذا يُطلق نظام التشغيل الإشعار في الوقت الصحيح بغض النظر عن إزاحة UTC.

نصيحة: دائماً أنشئ TZDateTime من tz.local وليس من tz.UTC حتى تُعالَج انتقالات التوقيت الصيفي بشفافية تامة.

جدولة إشعار لمرة واحدة

import 'package:timezone/timezone.dart' as tz;

Future<void> scheduleOneTimeNotification({
  required int id,
  required String title,
  required String body,
  required DateTime scheduledDate,
}) async {
  final tz.TZDateTime tzScheduled =
      tz.TZDateTime.from(scheduledDate, tz.local);

  const AndroidNotificationDetails androidDetails =
      AndroidNotificationDetails(
    'scheduled_channel',
    'Scheduled Notifications',
    channelDescription: 'One-time scheduled reminders',
    importance: Importance.high,
    priority: Priority.high,
  );
  const NotificationDetails notifDetails =
      NotificationDetails(android: androidDetails);

  await flutterLocalNotificationsPlugin.zonedSchedule(
    id,
    title,
    body,
    tzScheduled,
    notifDetails,
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
  );
}

// الاستخدام: تشغيل تذكير بعد 10 دقائق من الآن
void scheduleReminder() {
  final DateTime target =
      DateTime.now().add(const Duration(minutes: 10));
  scheduleOneTimeNotification(
    id: 42,
    title: 'تذكير بالاجتماع',
    body: 'اجتماعك اليومي يبدأ بعد 10 دقائق.',
    scheduledDate: target,
  );
}

إعداد الإشعارات المتكررة

للإشعارات التي تتكرر بإيقاع ثابت، استخدم periodicallyShow() (فترات بسيطة) أو zonedSchedule() مع معامل DateTimeComponents (تكرارات مرتبطة بالتقويم). كلا الأسلوبين يخدم احتياجات مختلفة:

  • periodicallyShow() — يُطلَق كل N ثانية/دقيقة/ساعة/يوم محسوباً من وقت الاستدعاء. بسيط لكن غير مدرك للتقويم.
  • zonedSchedule() + matchDateTimeComponents — يُطلَق في وقت محدد من اليوم (يومياً) أو يوم+وقت محدد (أسبوعياً). مدرك للتقويم ويحترم التوقيت الصيفي.

إشعارات يومية وأسبوعية متكررة

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;

/// يُطلَق كل يوم في الساعة [hour]:[minute] بالمنطقة الزمنية المحلية للجهاز.
Future<void> scheduleDailyNotification({
  required int id,
  required String title,
  required String body,
  required int hour,
  required int minute,
}) async {
  final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
  tz.TZDateTime scheduledDate = tz.TZDateTime(
    tz.local,
    now.year,
    now.month,
    now.day,
    hour,
    minute,
  );
  // إذا مضى الوقت اليوم، ابدأ من الغد
  if (scheduledDate.isBefore(now)) {
    scheduledDate = scheduledDate.add(const Duration(days: 1));
  }

  const AndroidNotificationDetails androidDetails =
      AndroidNotificationDetails(
    'daily_channel',
    'Daily Notifications',
    channelDescription: 'Recurring daily reminders',
    importance: Importance.defaultImportance,
  );
  const NotificationDetails notifDetails =
      NotificationDetails(android: androidDetails);

  await flutterLocalNotificationsPlugin.zonedSchedule(
    id,
    title,
    body,
    scheduledDate,
    notifDetails,
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
    // المفتاح: تكرار في نفس وقت اليوم يومياً
    matchDateTimeComponents: DateTimeComponents.time,
  );
}

/// يُطلَق كل أسبوع في [weekday] (DateTime.monday … .sunday) في الساعة [hour]:[minute].
Future<void> scheduleWeeklyNotification({
  required int id,
  required String title,
  required String body,
  required int weekday,
  required int hour,
  required int minute,
}) async {
  final tz.TZDateTime firstOccurrence = _nextInstanceOfWeekdayTime(
    weekday: weekday,
    hour: hour,
    minute: minute,
  );

  const AndroidNotificationDetails androidDetails =
      AndroidNotificationDetails(
    'weekly_channel',
    'Weekly Notifications',
    channelDescription: 'Recurring weekly reminders',
  );
  const NotificationDetails notifDetails =
      NotificationDetails(android: androidDetails);

  await flutterLocalNotificationsPlugin.zonedSchedule(
    id,
    title,
    body,
    firstOccurrence,
    notifDetails,
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
    // المفتاح: تكرار في نفس يوم الأسبوع + الوقت أسبوعياً
    matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime,
  );
}

tz.TZDateTime _nextInstanceOfWeekdayTime({
  required int weekday,
  required int hour,
  required int minute,
}) {
  final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
  tz.TZDateTime candidate = tz.TZDateTime(
    tz.local,
    now.year,
    now.month,
    now.day,
    hour,
    minute,
  );
  while (candidate.weekday != weekday || candidate.isBefore(now)) {
    candidate = candidate.add(const Duration(days: 1));
  }
  return candidate;
}

إلغاء الإشعارات المجدولة

يُعرَّف كل إشعار مجدول بالعدد الصحيح id الذي توفره. استخدم ذلك المعرّف لإلغاء إشعار بعينه، أو استدعِ cancelAll() لإزالة كل الإشعارات المعلقة دفعة واحدة.

  • flutterLocalNotificationsPlugin.cancel(id) — يلغي الإشعار بالمعرّف المحدد.
  • flutterLocalNotificationsPlugin.cancelAll() — يلغي جميع الإشعارات المعلقة.
  • flutterLocalNotificationsPlugin.pendingNotificationRequests() — يُعيد قائمة بجميع الإشعارات المنتظرة حتى تتمكن من فحصها أو إلغائها بشكل انتقائي.
تحذير: على Android 12 فأعلى (API 31+)، تتطلب التنبيهات الدقيقة صلاحية SCHEDULE_EXACT_ALARM أو USE_EXACT_ALARM في AndroidManifest.xml. بدونها سيُخفَّض AndroidScheduleMode.exactAllowWhileIdle تلقائياً إلى تنبيه غير دقيق. أعلن دائماً عن هذه الصلاحية، وللإصدار API 31 اطلب من المستخدم منحها عبر canScheduleExactNotifications().

خيارات AndroidScheduleMode

فهم أوضاع الجدولة المتاحة يساعدك في اختيار التوازن الصحيح بين الدقة وتأثير البطارية:

  • exact — يُطلَق في الوقت المحدد تماماً لكن قد يُؤجَّل إذا كان الجهاز في وضع Doze.
  • exactAllowWhileIdle — يُطلَق حتى أثناء وضع Doze، وهو الخيار الأكثر موثوقية للتذكيرات التي يراها المستخدم.
  • inexact — يجمّع نظام التشغيل التنبيه لتحقيق الكفاءة؛ قد يتأخر التسليم بدقائق. الأفضل للمهام الخلفية غير الحساسة.

ملخص

توفر واجهة الجدولة المنطقية في flutter_local_notifications تسليماً دقيقاً وصحيحاً من حيث المنطقة الزمنية. استخدم zonedSchedule() مع TZDateTime للتذكيرات لمرة واحدة، وأضف matchDateTimeComponents: DateTimeComponents.time للتكرار اليومي، أو DateTimeComponents.dayOfWeekAndTime للتكرار الأسبوعي. هيّئ دائماً حزمة timezone عند بدء التشغيل، وأعلن عن صلاحية التنبيه الدقيق على Android 12+، واحتفظ بمعرّفات الإشعارات حتى تتمكن من إلغائها بشكل نظيف.