قنوات المنصة والتكامل الأصلي

نظرة عامة على قنوات المنصة والبنية المعمارية

15 دقيقة الدرس 1 من 11

نظرة عامة على قنوات المنصة والبنية المعمارية

صُمِّم Flutter للعمل على منصات متعددة — Android وiOS والويب وسطح المكتب — بقاعدة كود Dart واحدة. غير أنه لا يمكن التعبير عن كل إمكانية في الجهاز بلغة Dart وحدها. تتطلب ميزات كالوصول إلى مستوى البطارية، أو استخدام Bluetooth، أو قراءة علامات NFC، أو التكامل مع حزم SDK الأصلية، استدعاءَ واجهات برمجة التطبيقات الأصلية للمنصة المتاحة فقط في Java/Kotlin على Android أو Objective-C/Swift على iOS. وتُمثِّل قنوات المنصة (Platform Channels) حلَّ Flutter لهذه المشكلة: جسرٌ نظيف محدَّد التعريف بين عالم Dart وعالم التطبيقات الأصلية.

ملاحظة: لا تحتاج إلى قنوات المنصة في معظم تطوير Flutter اليومي. نظام البيئة لمكونات Flutter الإضافية يُغلِّف مئات واجهات برمجة التطبيقات الأصلية كحزم على pub.dev. تبني قنوات المنصة حين لا تتوفر مكونة إضافية بعد، أو حين تحتاج إلى دمج حزمة SDK أصلية خاصة، أو حين تؤلِّف مكونتك الإضافية الخاصة.

لماذا توجد قنوات المنصة

يعمل محرك رسوم Flutter (Skia/Impeller) بالكامل في Dart ويرسم البكسل مباشرةً على لوحة رسم — متجاوِزاً نظام العرض الأصلي. يمنح هذا Flutter مظهره المتسق عبر المنصات. لكنه يعني أيضاً أن Flutter لا يستطيع استدعاء واجهات Android أو iOS مباشرةً. تحل قنوات المنصة هذه المعضلة بالعمل كـقناة لتمرير الرسائل بين عزلة Dart والخيط الرئيسي للمنصة المضيفة، مع انتقال بيانات متسلسلة عبر الحدود في كلا الاتجاهين.

البنية المعمارية ذات الثلاث طبقات

يتضمن تكامل قناة المنصة دائماً ثلاث طبقات بالضبط:

  • طبقة Dart — ودجت Flutter أو فئة الخدمة التي تستدعي القناة وتنتظر الاستجابة.
  • طبقة القناة — أنبوب منطقي مُسمَّى (مثلاً com.example.app/battery) يُسلسِل أسماء الدوال والوسائط باستخدام مشفِّر، ويوجِّهها عبر المُرسِل الثنائي.
  • الطبقة الأصلية — معالج MethodChannel مسجَّل في MainActivity.kt (Android) أو AppDelegate.swift (iOS) يستقبل الاستدعاء وينفِّذ الكود الأصلي ويُعيد نتيجة.
نصيحة: سمِّ قنواتك بأسلوب DNS عكسي (com.yourcompany.appname/feature) لتجنب التعارضات مع قنوات أخرى في التطبيقات الكبيرة أو المكونات الإضافية لأطراف ثالثة.

كيف تتدفق البيانات: المُرسِل الثنائي

يُسلسِل MessageCodec جميعَ البيانات العابرة لحدود المنصة. يُشحَن Flutter بثلاثة مشفِّرات:

  • StandardMessageCodec — يعالج bool وint وdouble وString وUint8List وList وMap. وهذا هو المشفِّر الافتراضي.
  • JSONMessageCodec — يُرمِّز القيم بتنسيق JSON بترميز UTF-8. أقل كفاءةً لكن مقروء بشرياً.
  • BinaryCodec — يمرِّر ByteData خاماً دون أي تكلفة ترميز؛ يُستخدم للدفق عالي الإنتاجية.

حين تستدعي دالةً على MethodChannel، يُسلِّم وقت تشغيل Dart مؤشر بايت إلى BinaryMessenger، الذي يُوجِّهه عبر محرك Flutter إلى المضيف الأصلي. يُفكِّك المضيف الشفرة، وينفِّذ الكود الأصلي، ويُرمِّز القيمة المُعادة، ثم يُسلِّم BinaryMessenger الردَّ إلى Future المنتظر في Dart.

الجانب Dart — تعريف MethodChannel واستدعاؤه

import 'package:flutter/services.dart';

class BatteryService {
  // يجب أن يتطابق اسم القناة تماماً مع الجانب الأصلي
  static const _channel = MethodChannel('com.example.myapp/battery');

  /// يُعيد مستوى البطارية الحالي (0-100) أو يرمي PlatformException.
  Future<int> getBatteryLevel() async {
    try {
      final int level = await _channel.invokeMethod<int>('getBatteryLevel') ?? -1;
      return level;
    } on PlatformException catch (e) {
      throw Exception('فشل الحصول على مستوى البطارية: ${e.message}');
    }
  }
}

أنواع القنوات

يوفِّر Flutter ثلاث فئات للقنوات، كل منها مناسبة لنمط تواصل مختلف:

  • MethodChannel — طلب/استجابة. تستدعي Dart دالةً مُسمَّاة وتتلقى ردَّاً واحداً بالضبط. مثالية للاستعلامات الفردية (مستوى البطارية، اللغة الحالية، حالة الإذن).
  • EventChannel — بث من الأصلي إلى Dart. تدفع الجهة الأصلية تدفقاً من الأحداث (قراءات المستشعر، تغيرات حالة الشبكة) تستهلكها Dart كـStream.
  • BasicMessageChannel — تبادل رسائل ثنائي الاتجاه بمشفِّر مخصَّص. يُستخدم لتبادل البيانات المنظَّمة الحرة في أي اتجاه.

مثال بسيط — استقبال تدفق EventChannel في Dart

import 'package:flutter/services.dart';

class ConnectivityService {
  static const _events = EventChannel('com.example.myapp/connectivity');

  /// يُصدِر true عند الاتصال، وfalse عند انقطاعه.
  Stream<bool> get onConnectivityChanged {
    return _events
        .receiveBroadcastStream()
        .map((event) => event as bool);
  }
}

// الاستخدام داخل StatefulWidget:
//   _subscription = ConnectivityService().onConnectivityChanged.listen(
//     (connected) => setState(() => _isOnline = connected),
//   );

نموذج الخيوط (Threading Model)

يُعدُّ فهم نموذج الخيوط أمراً بالغ الأهمية لكتابة كود قناة منصة صحيح:

  • يعمل كود Dart في عزلة Dart أحادية الخيط. لا توجد عمليات حجب؛ جميع استدعاءات القناة غير متزامنة.
  • على Android، يُستدعى معالجات القنوات على الخيط الرئيسي (UI) افتراضياً. يجب إرسال العمليات الأصلية طويلة الأمد يدوياً إلى خيط خلفي.
  • على iOS، يُستدعى المعالجات على الخيط الرئيسي. تنطبق نفس القاعدة — انقل العمل الثقيل إلى طابور خلفي.
  • يضمن محرك Flutter تسليم الردود إلى عزلة Dart بأمان بغض النظر عن الخيط الذي يُرسِل الرد.
تحذير: لا تُجرِ أبداً عمليات I/O حاجبة أو حسابات طويلة مباشرةً في معالج قناة المنصة على الخيط الرئيسي. سيؤدي ذلك إلى تجميد واجهة المستخدم. استخدم CoroutineScope على Android أو DispatchQueue.global() على iOS لتشغيل العمل بشكل غير متزامن، ثم استدعِ result.success(value) من أي خيط.

الخلاصة

تمنح قنوات المنصة Flutter جسراً منظَّماً وآمن الأنواع لواجهات برمجة التطبيقات الأصلية دون التضحية بالتناسق عبر المنصات الذي يجعل Flutter ذا قيمة. المفاهيم الرئيسية التي يجب تذكرها:

  • ثلاث طبقات: Dart، القناة (مشفِّر + مُرسِل)، معالج أصلي.
  • ثلاثة أنواع من القنوات: MethodChannel (RPC)، وEventChannel (بث)، وBasicMessageChannel (حر الشكل).
  • تُسلسَل البيانات بواسطة مشفِّر؛ يعالج المشفِّر الافتراضي (StandardMessageCodec) جميع أنواع Dart الأولية.
  • جميع استدعاءات القناة غير متزامنة في Dart؛ تعمل معالجات الجانب الأصلي على الخيط الرئيسي ويجب ألا تحجب.