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

الخدمات الأمامية في Android

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

الخدمات الأمامية في Android

الخدمة الأمامية (Foreground Service) هي مكوّن في Android يؤدي عمليات يلاحظها المستخدم، ويجب أن يعرض إشعاراً دائماً في شريط الحالة. على عكس خدمات الخلفية، تتمتع الخدمات الأمامية بأولوية أعلى وتكون أقل عرضة للإيقاف من قِبَل النظام عند نقص الذاكرة. وهي الأداة الصحيحة للمهام التي يجب أن تستمر في العمل حتى عندما يتنقل المستخدم بعيداً عن تطبيقك — مثل تشغيل الصوت، وتتبع موقع GPS، ورفع الملفات، ومؤقتات تمارين اللياقة البدنية.

ملاحظة: منذ Android 8.0 (API 26)، تخضع جميع خدمات الخلفية لقيود صارمة. إذا كنت بحاجة إلى عمل طويل الأمد يدركه المستخدم، فإن خدمة أمامية مع إشعار مرئي هي النهج الإلزامي. الخدمات الصامتة في الخلفية المُشغَّلة عبر startService() يتم تقييدها أو إيقافها.

كيف يتواصل Flutter مع خدمات Android

يعمل Flutter في عزل Dart (Dart isolate) فوق بيئة تشغيل Android. خدمات Android الأصلية مكتوبة بـ Java أو Kotlin وتعيش خارج طبقة Dart تماماً. يتواصل Flutter معها من خلال قنوات المنصة (Platform Channels) — تحديداً MethodChannel للاستدعاءات الفردية وEventChannel للبثوث المستمرة. الحزمة المُوصى بها في Flutter لتغليف هذه الآلية للخدمات الأمامية هي flutter_foreground_task. تتولى هذه الحزمة:

  • تشغيل وإيقاف Service الأصلية من Dart
  • إرسال بيانات المهمة من الخدمة إلى Flutter عبر منفذ العزل (isolate port)
  • إدارة إشعار الخلفية الإلزامي تلقائياً
  • معالجة أذونات FOREGROUND_SERVICE وPOST_NOTIFICATIONS

أذونات AndroidManifest الضرورية

قبل تشغيل أي كود، يجب التصريح بالأذونات الصحيحة وإدخال الخدمة في android/app/src/main/AndroidManifest.xml:

إضافات AndroidManifest.xml

<!-- الأذونات المطلوبة -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application ...>
  <!-- تسجيل مكوّن الخدمة الأمامية -->
  <service
    android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
    android:foregroundServiceType="location"
    android:stopWithTask="false" />
</application>
تحذير: يتطلب Android 14+ أن تتطابق سمة foregroundServiceType مع العمل الفعلي الذي تؤديه الخدمة (مثل location، أو mediaPlayback، أو dataSync). عدم التطابق يتسبب في خطأ ForegroundServiceStartNotAllowedException وقت التشغيل.

تهيئة مهمة الخلفية في Dart

تعرض حزمة flutter_foreground_task واجهة برمجية نظيفة في Dart. الخطوة الأولى هي استدعاء FlutterForegroundTask.init() مرة واحدة — عادةً في main() أو في initState() قبل تشغيل الخدمة. تمرر إليها كائن AndroidNotificationOptions الذي يصف الإشعار الدائم الذي سيراه المستخدم.

تهيئة وتشغيل خدمة أمامية

import 'package:flutter_foreground_task/flutter_foreground_task.dart';

// 1. تهيئة مرة واحدة (مثلاً في main أو دالة إعداد مخصصة)
void initForegroundTask() {
  FlutterForegroundTask.init(
    androidNotificationOptions: AndroidNotificationOptions(
      channelId: 'location_tracking_channel',
      channelName: 'Location Tracking',
      channelDescription: 'Tracks your position in the background.',
      channelImportance: NotificationChannelImportance.LOW,
      priority: NotificationPriority.LOW,
      iconData: const NotificationIconData(
        resType: ResourceType.mipmap,
        resPrefix: ResourcePrefix.ic,
        name: 'launcher',
      ),
    ),
    iosNotificationOptions: const IOSNotificationOptions(
      showNotification: true,
      playSound: false,
    ),
    foregroundTaskOptions: const ForegroundTaskOptions(
      interval: 5000,          // يُنفَّذ الاستدعاء كل 5 ثوانٍ
      isOnceEvent: false,
      autoRunOnBoot: true,     // إعادة التشغيل بعد إقلاع الجهاز
      allowWakeLock: true,
      allowWifiLock: true,
    ),
  );
}

// 2. تشغيل الخدمة مع تمرير دالة معالج المهمة (يجب أن تكون دالة عليا)
Future<bool> startForegroundTask() async {
  if (await FlutterForegroundTask.isRunningService) {
    return FlutterForegroundTask.restartService();
  }
  return FlutterForegroundTask.startService(
    notificationTitle: 'Location Tracking Active',
    notificationText: 'Tap to return to the app',
    callback: startCallback,   // يجب أن تكون دالة علياً (top-level)
  );
}

كتابة معالج المهمة

يعمل الاستدعاء داخل عزل Dart منفصل يُنتجه الخدمة الأصلية. يجب أن تكون دالة علياً (top-level function) — لا طريقة أو دالة مجهولة — حتى يتمكن العزل من تحديد موقعها. تنفذ صنفاً يرث من TaskHandler ويتجاوز ثلاث طرق لدورة الحياة:

تنفيذ TaskHandler

// نقطة الدخول العلياً — ضرورية لآلية العزل
@pragma('vm:entry-point')
void startCallback() {
  FlutterForegroundTask.setTaskHandler(LocationTaskHandler());
}

class LocationTaskHandler extends TaskHandler {
  int _elapsedSeconds = 0;

  // تُستدعى مرة واحدة عند بدء العزل
  @override
  Future<void> onStart(DateTime timestamp, SendPort? sendPort) async {
    // تهيئة الموارد: فتح قاعدة البيانات، الاشتراك في مجريات المستشعر...
    _elapsedSeconds = 0;
  }

  // تُستدعى بشكل متكرر وفق الفترة المحددة في ForegroundTaskOptions
  @override
  Future<void> onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {
    _elapsedSeconds += 5;

    // تحديث نص الإشعار الدائم
    FlutterForegroundTask.updateService(
      notificationTitle: 'التتبع: ${_elapsedSeconds} ثانية مضت',
      notificationText: 'خط العرض: 24.68  خط الطول: 46.72',
    );

    // إرسال البيانات إلى عزل واجهة المستخدم
    sendPort?.send({'elapsed': _elapsedSeconds});
  }

  // تُستدعى عند إيقاف الخدمة
  @override
  Future<void> onDestroy(DateTime timestamp, SendPort? sendPort) async {
    // تحرير الموارد: إغلاق المجريات، تفريغ المخابئ...
  }

  // تُستدعى عند ضغط المستخدم على زر إجراء الإشعار (إن كان مُهيَّأً)
  @override
  void onButtonPressed(String id) {
    if (id == 'stop') {
      FlutterForegroundTask.stopService();
    }
  }
}

استقبال البيانات في واجهة مستخدم Flutter

لعرض التحديثات الحية من الخدمة في شجرة الودجات، غلِّف الودجت الرئيسي بـ WithForegroundTask (الذي يتعامل مع ربط دورة الحياة) واستخدم ReceivePort للاستماع إلى الرسائل التي يرسلها sendPort?.send(...) داخل المعالج.

الاستماع إلى بيانات الخدمة في ودجت

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

  @override
  State<TrackingScreen> createState() => _TrackingScreenState();
}

class _TrackingScreenState extends State<TrackingScreen> {
  int _elapsed = 0;

  @override
  void initState() {
    super.initState();
    // تسجيل منفذ لاستقبال الرسائل من عزل الخدمة
    FlutterForegroundTask.addTaskDataCallback(_onReceiveData);
  }

  @override
  void dispose() {
    FlutterForegroundTask.removeTaskDataCallback(_onReceiveData);
    super.dispose();
  }

  void _onReceiveData(Object data) {
    if (data is Map<String, dynamic>) {
      setState(() => _elapsed = data['elapsed'] as int? ?? 0);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('تتبع الموقع')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('المدة: ${_elapsed} ثانية',
                style: Theme.of(context).textTheme.headlineMedium),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: startForegroundTask,
              child: const Text('تشغيل الخدمة'),
            ),
            ElevatedButton(
              onPressed: FlutterForegroundTask.stopService,
              child: const Text('إيقاف الخدمة'),
            ),
          ],
        ),
      ),
    );
  }
}

دورة حياة الخدمة

فهم دورة حياة الخدمة الأمامية يمنع تسرّب الموارد والسلوك غير المتوقع:

  • onCreate — يُنشأ كائن Service في Android؛ يُنتج عزل Dart؛ تُستدعى onStart() مرة واحدة.
  • onRepeatEvent — تُنفَّذ وفق الفترة الزمنية المحددة طوال حياة الخدمة.
  • onDestroy — تُشغَّل عند استدعاء stopService() أو إيقاف الخدمة من قِبَل النظام؛ حرّر جميع الموارد هنا.
  • autoRunOnBoot — إذا كانت true، تُعاد الخدمة تلقائياً بعد إعادة تشغيل الجهاز (تتطلب إذن RECEIVE_BOOT_COMPLETED ومستقبل بث في الملف — تتولى الحزمة ذلك تلقائياً).
نصيحة: استدعِ دائماً FlutterForegroundTask.stopService() في تدفق تسجيل الخروج أو إنهاء الجلسة في تطبيقك. إذا اكتفيت باستدعائه من الواجهة فقط، فإن المستخدم الذي يغلق التطبيق قسراً دون تسجيل الخروج سيبقي الخدمة تعمل إلى ما لا نهاية — مما يستنزف البطارية ويُبقي الإشعار ظاهراً.

خلاصة

تتيح خدمات Android الأمامية لتطبيق Flutter أداء عمل طويل الأمد — تتبع الموقع، بث الصوت، رفع الملفات — حتى عندما لا يكون التطبيق مرئياً. النقاط الرئيسية هي: التصريح بالخدمة وبـ foregroundServiceType الصحيح في الملف، وعرض إشعار دائم (إلزامي)، وتنفيذ TaskHandler في دالة علياً تعمل في عزلها الخاص، والتواصل مع الواجهة عبر SendPort. احرص دائماً على تحرير الموارد في onDestroy وإيقاف الخدمة صراحةً عند اكتمال العمل.