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