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

اختيار استراتيجية التكامل المناسبة وأفضل الممارسات

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

اختيار استراتيجية التكامل المناسبة وأفضل الممارسات

يوفر Flutter أربع آليات رئيسية للتواصل مع كود المنصة الأصلية: MethodChannel، وEventChannel، وBasicMessageChannel، وFFI (dart:ffi). اختيار نوع القناة الخاطئ يؤدي إلى تعقيد غير ضروري ومشاكل في الأداء وحالات تنافس يصعب تصحيحها. يمنحك هذا الدرس إطار قرار واضح، ويشرح قواعد التحديث (Threading) التي يجب على كل مطور احترامها، ويغطي أنماط معالجة الأخطاء الموثوقة، ويريك كيف تختبر كود القناة باستخدام تطبيقات وهمية (mocks).

نظرة سريعة على أنواع القنوات الأربع

قبل الاختيار، افهم ما صُمِّمت له كل قناة:

  • MethodChannel — استدعاءات طلب/استجابة. تستدعي Dart طريقة بالاسم، وتنفذ الجهة الأصلية العملية وترجع نتيجة واحدة (أو خطأ). الأفضل للعمليات الفردية كقراءة مستوى البطارية أو إطلاق حوار أصلي.
  • EventChannel — تدفقات مستمرة من الجانب الأصلي إلى Dart. تدفع الجهة الأصلية الأحداث في أي وقت؛ تستمع Dart عبر Stream. الأفضل لبيانات الاستشعار، وتغييرات الاتصال، أو تحديثات الموقع.
  • BasicMessageChannel — مراسلة ثنائية الاتجاه بين الأقران مع ترميز مخصص. يمكن لأي جانب إرسال رسائل اعتباطية في أي وقت. الأفضل للسيناريوهات الخفيفة أو عالية التردد أو ذات التسلسل المخصص.
  • FFI (dart:ffi) — استدعاءات دوال C/C++ مباشرة دون جولة تمرير رسائل. الأفضل للمكتبات الأصلية المكثفة حسابياً أو ذات الكمون الحرج (معالجة الصور، التشفير، الترميز).

إطار القرار: أي قناة تختار؟

اطرح ثلاثة أسئلة بالترتيب:

  1. هل تحتاج إلى تدفق مستمر من الأحداث الأصلية؟ إذا كانت الإجابة نعم، استخدم EventChannel.
  2. هل المنطق الأصلي مكتبة C/C++ خالصة بلا تدخل لواجهة المستخدم للمنصة؟ إذا كانت الإجابة نعم، استخدم FFI للاستدعاءات بدون حمل إضافي.
  3. هل تحتاج مراسلة منظمة ثنائية الاتجاه ليست بالضرورة طلب/استجابة؟ إذا كانت الإجابة نعم، استخدم BasicMessageChannel.
  4. في غير ذلك (الغالبية العظمى من الحالات): استخدم MethodChannel.
نصيحة: ابدأ بـ MethodChannel أولاً. فهو يغطي 80% من حالات استخدام المكونات الإضافية في العالم الحقيقي وله أغنى نظام بيئي من الأمثلة والأدوات.

قواعد التحديث (Threading) — المزلق الأكثر شيوعاً

تشترك جميع أنواع القنوات الأربع في قاعدة واحدة غير قابلة للتفاوض: يجب إجراء استدعاءات القناة على الخيط الرئيسي (UI) للمنصة. على Android هو الخيط الرئيسي؛ وعلى iOS هو قائمة الانتظار الرئيسية. انتهاك هذه القاعدة ينتج أخطاء تأكيد أو فساداً صامتاً في البيانات.

تحذير: إذا أطلق معالجك الأصلي خيطاً في الخلفية للقيام بعمل ثقيل، فيجب عليك العودة إلى الخيط الرئيسي قبل استدعاء result.success()، أو result.error()، أو إرسال قيمة لمستقبِل الحدث. الإخفاق في ذلك يُعدّ سلوكاً غير محدد.

مثال 1 — MethodChannel مع ترابط صحيح (جانب Dart)

import 'package:flutter/services.dart';

class BatteryService {
  static const _channel = MethodChannel('com.example.app/battery');

  /// تُرجع مستوى البطارية كنسبة مئوية (0-100).
  /// تُستدعى دائماً من عزل Dart المالك للقناة — آمنة.
  Future<int> getBatteryLevel() async {
    try {
      final int level = await _channel.invokeMethod<int>('getBatteryLevel')
          ?? -1;
      return level;
    } on PlatformException catch (e) {
      // الجانب الأصلي رمى خطأ — افحص الكود والرسالة
      debugPrint('Battery error [${e.code}]: ${e.message}');
      return -1;
    }
  }
}

// الاستخدام:
// final svc = BatteryService();
// final pct = await svc.getBatteryLevel();

مثال 2 — EventChannel لبيانات الاستشعار المستمرة

import 'package:flutter/services.dart';

class AccelerometerService {
  static const _channel = EventChannel('com.example.app/accelerometer');

  /// تبث خرائط تسارع XYZ. الجانب الأصلي يدفع الأحداث
  /// وفق جدوله الزمني الخاص؛ نعرض هنا تدفقاً مكتوباً بالأنواع.
  Stream<Map<String, double>> get accelerometerStream {
    return _channel
        .receiveBroadcastStream()
        .map((dynamic event) {
          final map = Map<String, dynamic>.from(event as Map);
          return {
            'x': (map['x'] as num).toDouble(),
            'y': (map['y'] as num).toDouble(),
            'z': (map['z'] as num).toDouble(),
          };
        });
  }
}

// الاستخدام في ودجت:
// StreamBuilder<Map<String, double>>(
//   stream: AccelerometerService().accelerometerStream,
//   builder: (context, snapshot) {
//     final data = snapshot.data;
//     return Text('X: ${data?[\'x\']?.toStringAsFixed(2)}');
//   },
// )

أنماط معالجة الأخطاء

الكود القوي للقناة يعالج ثلاثة أوضاع فشل:

  • PlatformException — يُرمى عندما يستدعي المعالج الأصلي result.error(code, message, details). دائماً التقطه وأظهر رسالة ذات معنى.
  • MissingPluginException — يُرمى عندما لا يكون هناك معالج أصلي مسجَّل لاسم القناة (شائع أثناء اختبارات الوحدة أو على منصات غير مدعومة). احرسه بـ try/catch أو تحقق من defaultBinaryMessenger في الاختبارات.
  • أحداث خطأ في التدفق — أخطاء EventChannel تصل كأحداث خطأ في التدفق؛ تعامل معها بـ stream.handleError() أو رد الاتصال onError في StreamBuilder.

اختبار كود القناة الأصلية باستخدام التطبيقات الوهمية

اختبارات الودجت والوحدة لا تصل إلى الكود الأصلي الحقيقي، لذا يجب أن تحاكي المرسل الثنائي. تعرض حزمة اختبار Flutter الميزة TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler لهذا الغرض تحديداً.

ملاحظة: دائماً استعِد التطبيق الوهمي إلى null في tearDown حتى تكون الاختبارات معزولة. ترك تطبيق وهمي مثبتاً قد يجعل الاختبارات غير ذات الصلة تنجح لأسباب خاطئة.

مثال 3 — اختبار وحدة MethodChannel بمعالج وهمي

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/battery_service.dart';

void main() {
  const channel = MethodChannel('com.example.app/battery');

  setUp(() {
    // تثبيت معالج أصلي وهمي
    TestDefaultBinaryMessengerBinding.instance
        .defaultBinaryMessenger
        .setMockMethodCallHandler(channel, (MethodCall call) async {
          if (call.method == 'getBatteryLevel') {
            return 72; // مستوى بطارية محاكى
          }
          throw PlatformException(
            code: 'NOT_IMPLEMENTED',
            message: 'Method ${call.method} not implemented',
          );
        });
  });

  tearDown(() {
    // التنظيف دائماً
    TestDefaultBinaryMessengerBinding.instance
        .defaultBinaryMessenger
        .setMockMethodCallHandler(channel, null);
  });

  test('getBatteryLevel تُرجع 72 من التطبيق الوهمي', () async {
    final svc = BatteryService();
    final level = await svc.getBatteryLevel();
    expect(level, 72);
  });
}

ملخص أفضل الممارسات

  • استخدم أسماء قنوات بنمط DNS العكسي (com.example.app/feature) لتجنب التعارض مع المكونات الإضافية التابعة لجهات خارجية.
  • اجعل واجهات القناة نحيفة — فئة Dart واحدة لكل قناة، مسؤولية واحدة.
  • دائماً تعامل مع PlatformException وMissingPluginException بشكل رشيق.
  • نفذ العمل الأصلي الثقيل في خيط خلفية، لكن أجب دائماً على الخيط الرئيسي.
  • اكتب اختبارات وحدة قائمة على المحاكاة لكل طريقة قناة — لا تتخطَّ هذه الخطوة في CI.
  • فضِّل FFI على أي نوع قناة عند استدعاء C/C++ بلا حاجة لترابط واجهة المستخدم للمنصة.
النقطة الرئيسية: MethodChannel هو الاختيار الافتراضي للاستدعاءات الأصلية الفردية. استخدم EventChannel للتدفقات من الجانب الأصلي إلى Dart، وBasicMessageChannel للمراسلة المخصصة بين الأقران، وFFI للوصول إلى مكتبات C بدون حمل إضافي. بغض النظر عن نوع القناة، استدعِ القنوات دائماً على الخيط الرئيسي، وتعامل مع جميع أنواع الاستثناءات، وحاكِ القنوات في الاختبارات.