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

كتابة مكوّن Flutter إضافي: التنفيذ الأصلي على Android وiOS

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

كتابة مكوّن Flutter إضافي: التنفيذ الأصلي على Android وiOS

في الدرس السابق عرّفت واجهة Dart نظيفة لمكوّنك الإضافي ووصّلت MethodChannel على الجانب Flutter. الآن حان الوقت لإحياء تلك القناة بكتابة التنفيذات الأصلية: صنف Kotlin لنظام Android وصنف Swift لنظام iOS. يستعرض هذا الدرس دورة الحياة الكاملة — من تسجيل المكوّن الإضافي مع محرّك Flutter إلى توزيع استدعاءات الأساليب وإعادة النتائج إلى Dart.

كيف يعثر Flutter على المكوّن الإضافي

عند بدء تشغيل Flutter على الجهاز يُشغّل FlutterEngine. يجب على كل مكوّن إضافي تسجيل نفسه مع ذلك المحرّك حتى يعرف المحرك أي صنف أصلي يعالج اسم القناة. على Android يحدث هذا داخل صنف يُنفّذ FlutterPlugin؛ وعلى iOS يحدث داخل صنف يوافق بروتوكول FlutterPlugin في Swift. يُعلن ملف pubspec.yaml الخاص بحزمة المكوّن الإضافي عن صنف نقطة الدخول لكل منصة تحت مفتاح flutter.plugin.platforms.

ملاحظة: إذا أنشأت مكوّنك الإضافي باستخدام flutter create --template=plugin، فإن السقالة تحتوي بالفعل على كود التسجيل الأساسي. فهم ما يفعله هذا الكود الأساسي أمر ضروري قبل تعديله.

التنفيذ على Android بلغة Kotlin

يجب على صنف المكوّن الإضافي لنظام Android تنفيذ FlutterPlugin وMethodCallHandler. توفر واجهة FlutterPlugin استدعاءين لدورة الحياة — onAttachedToEngine وonDetachedFromEngine — حيث تُنشئ القناة وتُفككها. توفر واجهة MethodCallHandler طريقة onMethodCall، حيث تُحوّل على اسم الأسلوب وتستدعي كود المنصة المقابل.

مكوّن Android — BatteryPlugin.kt

package com.example.battery_info

import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
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
    private lateinit var context: Context

    // يُستدعى عند ربط المكوّن الإضافي بمحرّك Flutter.
    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        context = binding.applicationContext
        // يجب أن يطابق اسم القناة جانب Dart تماماً.
        channel = MethodChannel(binding.binaryMessenger, "com.example/battery_info")
        channel.setMethodCallHandler(this)
    }

    // توزيع استدعاءات أساليب Dart الواردة إلى الأسلوب الأصلي الصحيح.
    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "getBatteryLevel" -> {
                val level = getBatteryLevel()
                if (level != -1) {
                    result.success(level)          // يُعيد Int إلى Dart
                } else {
                    result.error(
                        "UNAVAILABLE",
                        "Battery level not available.",
                        null
                    )
                }
            }
            "isCharging" -> {
                result.success(isCharging())       // يُعيد Boolean إلى Dart
            }
            else -> result.notImplemented()
        }
    }

    private fun getBatteryLevel(): Int {
        val batteryManager = context
            .getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        return batteryManager
            .getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    }

    private fun isCharging(): Boolean {
        val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        val intent = context.registerReceiver(null, filter) ?: return false
        val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
        return status == BatteryManager.BATTERY_STATUS_CHARGING ||
               status == BatteryManager.BATTERY_STATUS_FULL
    }

    // يُستدعى عند تدمير المحرّك. دائماً حرّر القناة هنا.
    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }
}
نصيحة: استدعِ دائماً channel.setMethodCallHandler(null) داخل onDetachedFromEngine. الإخفاق في ذلك يُسرّب مرجع المعالج ويمكن أن يُسبب أعطالاً عند إعادة استخدام المحرّك (مثلاً في سيناريوهات إضافة-إلى-تطبيق).

التنفيذ على iOS بلغة Swift

على iOS يعكس النمط Android لكنه يستخدم محاور Swift. يوافق الصنف كلاً من NSObject وبروتوكول FlutterPlugin. تُستدعى الطريقة الثابتة register(with:) — المعادل iOS لـ onAttachedToEngine — تلقائياً من قِبل محرّك Flutter. تخزن القناة كخاصية حتى تتمكن من إلغائها أثناء إيقاف التشغيل.

مكوّن iOS — BatteryPlugin.swift

import Flutter
import UIKit

public class BatteryPlugin: NSObject, FlutterPlugin {

    // MARK: - التسجيل (يُستدعى مرة واحدة من محرّك Flutter)
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(
            name: "com.example/battery_info",        // يجب أن يطابق Dart
            binaryMessenger: registrar.messenger()
        )
        let instance = BatteryPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
    }

    // MARK: - معالج استدعاء الأسلوب
    public func handle(
        _ call: FlutterMethodCall,
        result: @escaping FlutterResult
    ) {
        switch call.method {
        case "getBatteryLevel":
            let level = getBatteryLevel()
            if level >= 0 {
                result(level)                        // NSNumber (Int) إلى Dart
            } else {
                result(FlutterError(
                    code: "UNAVAILABLE",
                    message: "Battery level not available.",
                    details: nil
                ))
            }
        case "isCharging":
            result(isCharging())                     // Bool إلى Dart
        default:
            result(FlutterMethodNotImplemented)
        }
    }

    // MARK: - المساعدات الأصلية
    private func getBatteryLevel() -> Int {
        UIDevice.current.isBatteryMonitoringEnabled = true
        let level = UIDevice.current.batteryLevel
        UIDevice.current.isBatteryMonitoringEnabled = false
        guard level >= 0 else { return -1 }
        return Int(level * 100)
    }

    private func isCharging() -> Bool {
        UIDevice.current.isBatteryMonitoringEnabled = true
        let state = UIDevice.current.batteryState
        UIDevice.current.isBatteryMonitoringEnabled = false
        return state == .charging || state == .full
    }
}

ربط واجهة Dart بالجانب الأصلي

مع وجود كلا الصنفين الأصليين في مكانهما، يمتلك صنف Dart API الذي كتبته في الدرس السابق الآن نظيراً حياً على كل منصة. استدعاء مثل await BatteryInfo.getBatteryLevel() يسافر عبر الترميز، ويعبر حدود المنصة، ويصل إلى onMethodCall (Android) أو handle(_:result:) (iOS)، وتتدفق النتيجة الأصلية مرة أخرى كـ Future في Dart.

واجهة Dart — battery_info.dart (مراجعة)

import 'package:flutter/services.dart';

class BatteryInfo {
  // اسم القناة هو العقد بين Dart والكود الأصلي.
  static const MethodChannel _channel =
      MethodChannel('com.example/battery_info');

  /// يُعيد مستوى البطارية كعدد صحيح (0–100).
  static Future<int> getBatteryLevel() async {
    final int level = await _channel.invokeMethod('getBatteryLevel');
    return level;
  }

  /// يُعيد true إذا كان الجهاز يشحن حالياً.
  static Future<bool> isCharging() async {
    final bool charging = await _channel.invokeMethod('isCharging');
    return charging;
  }
}

تسجيل المكوّن الإضافي في pubspec.yaml

لكي يكتشف Flutter مكوّنك الإضافي تلقائياً، يجب أن يُعلن ملف pubspec.yaml الخاص بحزمة المكوّن الإضافي عن نقطتَي الدخول لكلتا المنصتين. بدون هذا الإعلان لن يستدعي المحرّك أبداً register(with:) أو onAttachedToEngine وستظل جميع استدعاءات الأساليب دون إجابة.

  • Android — اضبط dartPluginClass (لتسجيل Dart) و/أو pluginClass (اسم صنف Kotlin/Java) تحت flutter.plugin.platforms.android.
  • iOS — اضبط pluginClass لاسم صنف Swift/ObjC تحت flutter.plugin.platforms.ios.
  • يجب أن تطابق سلسلة pluginClass اسم الصنف تماماً (حساسة لحالة الأحرف).
تحذير: إذا أعدت تسمية صنف Kotlin أو Swift، يجب عليك تحديث pubspec.yaml ليطابق ذلك. عدم التطابق يُسبب فشلاً صامتاً في وقت التشغيل — ستُعيد استدعاءات قناتك MissingPluginException دون أي خطأ ترجمة مرئي.

اختبار الرحلة الكاملة ذهاباً وإياباً

بعد توصيل الجانبين، شغّل التطبيق على جهاز حقيقي (المحاكيات تُبلّغ عن مستوى بطارية -1 على iOS) واستدعِ Dart API. استخدم flutter run --verbose لمراقبة حركة قناة المنصة في وحدة التحكم. للاختبارات الوحدوية، احتَكِ بالقناة باستخدام TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler حتى لا يُستدعى الكود الأصلي أثناء التكامل المستمر.

ملخص

يتطلب تنفيذ الجانب الأصلي لمكوّن Flutter إضافي ثلاث خطوات منسّقة: (1) تنفيذ FlutterPlugin + MethodCallHandler في Kotlin وتسجيل القناة في onAttachedToEngine؛ (2) الموافقة على FlutterPlugin في Swift وتسجيل القناة في الطريقة الثابتة register(with:)؛ (3) التأكد من أن اسم القناة وسلاسل الأساليب متطابقة عبر الملفات الثلاثة. مع هذا في مكانه، تكون Dart API الخاصة بك مدعومة بأجهزة الجهاز الحقيقية على كل منصة يدعمها Flutter.