كتابة مكوّن Flutter إضافي: التنفيذ الأصلي على Android وiOS
كتابة مكوّن 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اسم الصنف تماماً (حساسة لحالة الأحرف).
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.