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

BasicMessageChannel: التراسل الثنائي الاتجاه للبيانات العشوائية

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

BasicMessageChannel: التراسل الثنائي الاتجاه للبيانات العشوائية

BasicMessageChannel هو الأكثر مرونة بين أنواع قنوات المنصة الثلاثة في Flutter. على عكس MethodChannel (الذي يتبع نموذج استدعاء-استجابة RPC) وEventChannel (الذي يبث الأحداث من الكود الأصلي إلى Dart)، يتيح BasicMessageChannel لـ أي من الطرفين — Dart أو الكود الأصلي — بدء رسالة في أي وقت، ويمكن للمستقبل اختيارياً إرسال رد. هذا يجعله مثالياً للتواصل الحر ذي الكمون المنخفض في الاتجاهين حيث لا يكون أي من الطرفين "مستدعياً" أو "مستمعاً" بالمعنى الصارم.

التمييز الرئيسي: مع MethodChannel، تستدعي Dart الكود الأصلي وتنتظر نتيجة. مع BasicMessageChannel، يمكن لكل من Dart والكود الأصلي إرسال رسائل لبعضهما بشكل مستقل، وكل رسالة قد تحمل ردًا عشوائياً.

متى تختار BasicMessageChannel

استخدم BasicMessageChannel عندما:

  • يحتاج الكود الأصلي لدفع بيانات إلى Dart دون أن تستدعي Dart أولاً أي دالة
  • تحتاج Dart لإرسال بيانات منظمة عشوائية إلى الكود الأصلي وتلقي رد منظم
  • تريد تبادل أنواع غير قياسية (ثنائيات، كائنات مخصصة) باستخدام MessageCodec مخصص
  • تحتاج إلى أنبوب خفيف الوزن ثنائي الاتجاه دون التعقيد الزائد لدفق الأحداث الكامل

خيارات MessageCodec

كل BasicMessageChannel مُحدَّد بـ MessageCodec<T> يُسلسل الرسائل إلى بايتات ويُفك تسلسلها على الطرف الآخر. يوفر Flutter أربعة ترميزات مدمجة:

  • StandardMessageCodec — الافتراضي؛ يعالج null وbool وint وdouble وString وUint8List وInt32List وInt64List وFloat64List وList وMap
  • JSONMessageCodec — يُشفر ويُفك التشفير إلى/من JSON؛ مريح عندما يتعامل الكود الأصلي مع JSON بالفعل
  • BinaryCodec — يمرر ByteData الخام بدون أي تشفير إضافي؛ الأسرع للحمولات الثنائية
  • StringCodec — نصوص UTF-8 عادية؛ الأبسط للبروتوكولات النصية فقط

يمكنك أيضاً تنفيذ MessageCodec<T> مباشرةً لدعم تنسيقات أسلاك مخصصة (مثل protobuf أو MessagePack).

جانب Dart: الإرسال والاستقبال

أعلن عن القناة مرة واحدة، ثم استخدم send() لدفع رسالة (وانتظار الرد الاختياري) وsetMessageHandler() لاستقبال الرسائل التي يبدأها الكود الأصلي.

مثال 1 — Dart ترسل رسالة وتتلقى رداً

import 'dart:async';
import 'package:flutter/services.dart';

// أعلن عن قناة مُحدَّدة النوع باستخدام الترميز القياسي
const _channel = BasicMessageChannel<Object?>(
  'com.example.app/settings',
  StandardMessageCodec(),
);

class NativeSettingsService {
  /// يُستدعى عند بدء التشغيل: اطلب من الكود الأصلي خريطة الإعدادات الحالية.
  Future<Map<Object?, Object?>?> fetchSettings() async {
    final reply = await _channel.send({'action': 'getSettings'});
    if (reply is Map) {
      return reply as Map<Object?, Object?>;
    }
    return null;
  }

  /// سجّل معالجاً حتى يتمكن الكود الأصلي من دفع الإعدادات المحدّثة إلى Dart.
  void listenForUpdates(void Function(Map<Object?, Object?>) onUpdate) {
    _channel.setMessageHandler((message) async {
      if (message is Map) {
        onUpdate(message as Map<Object?, Object?>);
      }
      // أعد null أو نص إقرار
      return 'ack';
    });
  }
}
نصيحة: تُعيد send() Future يُحلّ برد الكود الأصلي، أو null إذا لم يرد المعالج الأصلي. تعامل دائماً مع حالة null لتجنب أخطاء LateInitializationError غير المتوقعة.

جانب الكود الأصلي: Android (Kotlin)

في Android، كرر إعلان القناة في MainActivity (أو أي حاضن لـ FlutterEngine). استخدم setMessageHandler للاستقبال من Dart، واستدعِ send على مثيل القناة لدفع رسائل غير مطلوبة إلى Dart.

مثال 2 — معالج Kotlin في Android + دفع غير مطلوب

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.StandardMessageCodec

class MainActivity : FlutterActivity() {

    private lateinit var settingsChannel: BasicMessageChannel<Any?>

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        settingsChannel = BasicMessageChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            "com.example.app/settings",
            StandardMessageCodec.INSTANCE
        )

        // معالجة الرسائل القادمة من Dart
        settingsChannel.setMessageHandler { message, reply ->
            if (message is Map<*, *> && message["action"] == "getSettings") {
                val settings = mapOf(
                    "theme" to "dark",
                    "fontSize" to 16,
                    "language" to "en"
                )
                reply.reply(settings)   // أرسل الرد إلى Dart
            } else {
                reply.reply(null)
            }
        }
    }

    // استدعِ هذه الدالة من أي مكان في كود Android لدفع بيانات إلى Dart
    fun pushSettingsUpdate(newSettings: Map<String, Any>) {
        settingsChannel.send(newSettings) { reply ->
            // اختياري: تعامل مع إقرار Dart
            println("Dart ack: $reply")
        }
    }
}

جانب الكود الأصلي: iOS (Swift)

واجهة برمجة Swift متماثلة. سجّل FlutterBasicMessageChannel في AppDelegate واستدعِ sendMessage لبدء رسائل من iOS إلى Dart.

مثال 3 — معالج Swift في iOS

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

    var settingsChannel: FlutterBasicMessageChannel?

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller = window?.rootViewController as! FlutterViewController
        let messenger = controller.binaryMessenger

        settingsChannel = FlutterBasicMessageChannel(
            name: "com.example.app/settings",
            binaryMessenger: messenger,
            codec: FlutterStandardMessageCodec.sharedInstance()
        )

        // استقبال الرسائل من Dart
        settingsChannel?.setMessageHandler { message, reply in
            if let dict = message as? [String: Any],
               dict["action"] as? String == "getSettings" {
                reply(["theme": "dark", "fontSize": 16, "language": "en"])
            } else {
                reply(nil)
            }
        }

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // دفع تحديث غير مطلوب من iOS إلى Dart
    func pushSettingsUpdate(_ settings: [String: Any]) {
        settingsChannel?.sendMessage(settings) { reply in
            print("Dart ack: \(reply ?? "none")")
        }
    }
}
تحذير: يجب إجراء جميع استدعاءات BasicMessageChannel على الخيط الرئيسي للمنصة على الجانب الأصلي، ويجب إعداد القناة بعد تهيئة محرك Flutter بالكامل. استدعاء send قبل أن يكون المحرك جاهزاً يُسقط الرسالة بصمت.

مقارنة أنواع القنوات الثلاثة

فهم متى تستخدم كل نوع قناة يمنع الإفراط في التصميم الهندسي:

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

الخلاصة

يملأ BasicMessageChannel الفجوة بين نموذج RPC الصارم لـ MethodChannel ودفق EventChannel أحادي الاتجاه. باختيار MessageCodec المناسب — قياسي، أو JSON، أو ثنائي، أو مخصص — يمكنك تبادل أي بيانات منظمة بكفاءة بين Dart والكود الأصلي، مع إمكانية أي من الطرفين بدء التواصل في أي وقت.

النقطة الرئيسية: عندما تحتاج لأن يدفع الكود الأصلي بيانات عشوائية إلى Dart (أو العكس) دون استدعاء Dart مسبق، BasicMessageChannel مع StandardMessageCodec هو أداتك المثلى. سجّل المعالج مبكراً، احتفظ بأسماء القنوات مُنسوبة (مثل com.yourapp/channelname)، وتعامل دائماً مع الردود الفارغة null بسلاسة.