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

MethodChannel: التنفيذ على Android بـ Kotlin

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

MethodChannel: التنفيذ على Android بـ Kotlin

عندما يحتاج Flutter إلى استدعاء وظيفة Android الأصلية — مثل قراءة مستوى بطارية الجهاز، أو الوصول المباشر إلى كاميرا الجهاز، أو استدعاء SDK منصة معينة — فإنك تُسجِّل معالج MethodChannel على الجانب الأندرويدي. يتناول هذا الدرس كيفية تنفيذ ذلك المعالج داخل FlutterActivity (أو FlutterPlugin) باستخدام Kotlin، وربطه بشكل صحيح، وإرجاع النتائج أو الأخطاء المنظَّمة إلى Dart.

أين يوجد الكود الأندرويدي

في مشروع Flutter قياسي، يقع كود المضيف الأندرويدي في android/app/src/main/kotlin/…/MainActivity.kt. نقطة الدخول هي فئة ترث من FlutterActivity. تُعيد تعريف configureFlutterEngine للحصول على الوصول إلى FlutterEngine وتسجيل قنواتك هناك.

ملاحظة: configureFlutterEngine هو الخطاف الصحيح في دورة الحياة لتسجيل القنوات. تجنَّب فعل ذلك في onCreate لأن المحرك قد لا يكون مُهيَّأً بالكامل في تلك المرحلة.

تسجيل MethodChannel في MainActivity.kt

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {

    private val CHANNEL = "com.example.myapp/battery"

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

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            CHANNEL
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getBatteryLevel" -> {
                    val batteryLevel = getBatteryLevel()
                    if (batteryLevel != -1) {
                        result.success(batteryLevel)
                    } else {
                        result.error(
                            "UNAVAILABLE",
                            "Battery level not available.",
                            null
                        )
                    }
                }
                else -> result.notImplemented()
            }
        }
    }
}

اللامبدا الخاص بـ MethodCallHandler

يستقبل اللامبدا الممرَّر إلى setMethodCallHandler معاملَين في كل مرة تستدعي فيها Dart القناة:

  • call: MethodCall — يحتوي على call.method (اسم السلسلة النصية) وcall.arguments (أي قيمة مُرسَلة من Dart، من النوع Any?).
  • result: MethodChannel.Result — كائن رد النداء الذي تستخدمه للرد مرة واحدة بالضبط: result.success(value)، أو result.error(code, message, details)، أو result.notImplemented().
تحذير: يجب عليك استدعاء واحدة بالضبط من success أو error أو notImplemented لكل استدعاء. عدم استدعاء أي منها يُبقي Future الخاص بـ Dart معلَّقاً إلى الأبد؛ واستدعاء أكثر من واحدة يُلقي استثناءً في وقت التشغيل.

استخراج المعاملات بأمان

يمكن لـ Dart تمرير المعاملات كـ Map أو List أو قيمة بدائية. على جانب Kotlin، حوِّل call.arguments إلى النوع المتوقع. استخدم المساعد المكتوب call.argument<T>(key) لمعاملات الخريطة لتجنُّب التحويلات غير الآمنة.

معالجة المعاملات وإرجاع نتيجة

"greet" -> {
    // أرسلت Dart: {'name': 'Flutter'}
    val name: String? = call.argument<String>("name")
    if (name == null) {
        result.error("MISSING_ARG", "Argument 'name' is required.", null)
        return@setMethodCallHandler
    }
    result.success("Hello from Kotlin, $name!")
}

"multiply" -> {
    val a: Int? = call.argument<Int>("a")
    val b: Int? = call.argument<Int>("b")
    if (a == null || b == null) {
        result.error("MISSING_ARG", "Both 'a' and 'b' are required.", null)
        return@setMethodCallHandler
    }
    result.success(a * b)
}

التنفيذ كـ FlutterPlugin

للحصول على كود أصلي قابل لإعادة الاستخدام (مثل مكتبة موزَّعة عبر pub.dev)، نفِّذ واجهة FlutterPlugin بدلاً من تضمين المنطق داخل MainActivity. هذا يفصل تسجيل القناة عن النشاط المضيف ويُمكِّن البرنامج المساعد من التسجيل التلقائي.

  • نفِّذ FlutterPlugin وأعِد تعريف onAttachedToEngine وonDetachedFromEngine.
  • سجِّل القناة في onAttachedToEngine باستخدام FlutterPluginBinding المُقدَّم.
  • أَلغِ التسجيل (اضبط المعالج على null) في onDetachedFromEngine لمنع تسريب الذاكرة.

نمط تسجيل FlutterPlugin

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

class BatteryPlugin : FlutterPlugin, MethodCallHandler {

    private lateinit var channel: MethodChannel

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(
            binding.binaryMessenger,
            "com.example.myapp/battery"
        )
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "getBatteryLevel" -> result.success(getNativeBatteryLevel())
            else              -> result.notImplemented()
        }
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null) // تجنُّب تسريب الذاكرة
    }

    private fun getNativeBatteryLevel(): Int {
        // ... تنفيذ المنصة
        return 87
    }
}

اعتبارات الخيوط (Threading)

بشكل افتراضي، يُستدعى MethodCallHandler على الخيط الرئيسي (خيط واجهة المستخدم). إذا كان عملك الأصلي طويل الأمد (إدخال/إخراج ملفات، شبكة، Bluetooth)، فانقله إلى خيط خلفي ثم استدعِ result.success() مجدداً على الخيط الرئيسي باستخدام Handler(Looper.getMainLooper()).post { … }، أو استخدم كوروتينات Kotlin مع Dispatchers.Main للرد.

نصيحة: يجب أن تتطابق أسماء القنوات تماماً بين Dart وKotlin — بما في ذلك حالة الأحرف وكل شرطة مائلة. خطأ شائع هو وجود شرطة مائلة زائدة أو خطأ إملائي يُرسل صامتاً جميع الاستدعاءات إلى notImplemented. عرِّف الاسم في ثابت مشترك أو اجعله مرئياً في كلا الملفَّين لسهولة المقارنة.

ملخص

لتنفيذ معالج MethodChannel على Android: أعِد تعريف configureFlutterEngine في MainActivity (أو نفِّذ FlutterPlugin للإضافات القابلة لإعادة الاستخدام)؛ أنشئ MethodChannel بالمُرسِل الثنائي واسم قناة مطابق؛ زوِّد اللامبدا الخاص بـ setMethodCallHandler الذي يُوزِّع على call.method؛ ارد بواحدة بالضبط من result.success أو result.error أو result.notImplemented؛ ونظِّف بضبط المعالج على null عند فصل المحرك.