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

كتابة مكوّن Flutter إضافي: هيكل الحزمة وواجهة Dart

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

كتابة مكوّن Flutter إضافي: هيكل الحزمة وواجهة Dart

مكوّن Flutter الإضافي (Flutter plugin) هو حزمة Dart متخصصة تُغلّف وظائف خاصة بالمنصة (native) وتعرضها عبر واجهة Dart نظيفة. على عكس حزمة Dart الخالصة، يحتوي المكوّن على كود أصلي لمنصة واحدة أو أكثر (Android وiOS وmacOS وWindows وLinux والويب) إلى جانب طبقة Dart. إن فهم كيفية إنشاء الهيكل وتصميم واجهة Dart API للمكوّن هو أساس كل مشروع تكامل أصلي.

إنشاء مكوّن إضافي متحد (Federated Plugin)

النهج الحديث الموصى به هو المكوّن المتحد (federated plugin): يتم تقسيم الوظائف عبر حزم Dart متعددة حتى يمكن تنفيذ كل منصة بشكل مستقل دون المساس بكود المنصات الأخرى. استخدم Flutter CLI لتوليد الهيكل:

إنشاء هيكل مكوّن متحد

# توليد الحزمة الموجهة للتطبيق (التي يستوردها مطورو التطبيقات)
flutter create --template=plugin \
  --org com.example \
  --platforms android,ios,macos \
  my_battery_info

# الأمر يُنتج الملفات والمجلدات التالية:
# my_battery_info/
#   lib/my_battery_info.dart          <-- واجهة Dart العامة
#   lib/src/my_battery_info_platform_interface.dart
#   android/                          <-- الكود الأصلي لـ Android
#   ios/                              <-- الكود الأصلي لـ iOS
#   macos/                            <-- الكود الأصلي لـ macOS
#   example/                          <-- تطبيق Flutter توضيحي
#   pubspec.yaml
#   CHANGELOG.md  README.md  LICENSE
ملاحظة: لتخطيط متحد كامل، تُنشئ أيضاً حزمة my_battery_info_platform_interface (العقد المجرد) وحزمة تنفيذ لكل منصة (مثل my_battery_info_android). القالب --template=plugin_ffi يُولّد هيكل مكوّن Dart FFI بدلاً من القائم على القنوات.

فهم هيكل المجلدات

بعد إنشاء الهيكل، الحزم الثلاث الأكثر أهمية في المكوّن المتحد هي:

  • الحزمة الموجهة للتطبيق (my_battery_info) — الحزمة التي يضيفها مطورو التطبيقات إلى pubspec.yaml. تُعيد تصدير واجهة المنصة وتستدعي التنفيذ المسجّل.
  • حزمة واجهة المنصة (my_battery_info_platform_interface) — تُعرّف الفئة المجردة MyBatteryInfoPlatform التي يجب على كل تنفيذ منصة توسيعها. تعتمد فقط على Dart وحزمة plugin_platform_interface.
  • حزم تنفيذ المنصة (my_battery_info_android، my_battery_info_ios، …) — كل منها يوفر فئة فرعية محددة من MyBatteryInfoPlatform ويحتوي على الكود الأصلي لمنصة واحدة فقط.
نصيحة: الاحتفاظ بالكود الأصلي في حزم تنفيذ منفصلة يعني أن مطور Windows الذي يضيف مكوّنك لا يحتاج لتنزيل Android SDKs. كما يتيح للمجتمع نشر تنفيذات بديلة دون تشعيب (fork) المكوّن بالكامل.

تصميم واجهة Dart العامة

ملف lib/my_battery_info.dart في الحزمة الموجهة للتطبيق هو السطح العام الوحيد الذي يراه المستخدمون النهائيون. احتفظ به رفيعاً: أوكل كل استدعاء إلى مُفرد واجهة المنصة. تتبع واجهة Dart الجيدة هذه الاتفاقيات:

  • يُفضَّل استخدام توابع ثابتة أو على مستوى أعلى تُعيد قيم Future<T> مكتوبة بقوة.
  • ارمِ استثناءات مسماة وموثقة بدلاً من إرجاع قيم nullable كبدائل.
  • اجعل الفئة final أو استخدم مُنشئ factory حتى لا يتمكن المستخدمون من توسيعها عن طريق الخطأ.
  • وثّق كل رمز عام بتعليق ثلاثي الشرطة ///.

واجهة Dart الموجهة للتطبيق — lib/my_battery_info.dart

import 'package:my_battery_info_platform_interface/my_battery_info_platform_interface.dart';

/// يوفر الوصول إلى مستوى بطارية الجهاز وحالة الشحن.
///
/// جميع التوابع غير متزامنة؛ تُفوّض إلى التنفيذ الخاص بالمنصة
/// المسجّل عبر [MyBatteryInfoPlatform.instance].
class MyBatteryInfo {
  // منع إنشاء الكائنات — جميع الأعضاء ثابتة.
  const MyBatteryInfo._();

  /// يُعيد مستوى البطارية الحالي كنسبة مئوية (0–100).
  ///
  /// يرمي [BatteryUnavailableException] إذا كان الجهاز لا يُبلّغ
  /// عن معلومات البطارية (مثل أجهزة سطح المكتب بدون بطارية).
  static Future<int> getBatteryLevel() {
    return MyBatteryInfoPlatform.instance.getBatteryLevel();
  }

  /// يُعيد `true` إذا كان الجهاز يتشحن حالياً.
  static Future<bool> isCharging() {
    return MyBatteryInfoPlatform.instance.isCharging();
  }

  /// تدفق من تحديثات مستوى البطارية (يُصدر عند كل تغيير بنسبة 1%).
  static Stream<int> get batteryLevelStream {
    return MyBatteryInfoPlatform.instance.batteryLevelStream;
  }
}

/// يُرمى عندما لا تتوفر معلومات البطارية على الجهاز الحالي.
class BatteryUnavailableException implements Exception {
  const BatteryUnavailableException([this.message]);
  final String? message;

  @override
  String toString() =>
      'BatteryUnavailableException${message != null ? ': $message' : ''}';
}

عقد واجهة المنصة

تُعرّف حزمة واجهة المنصة الفئة المجردة التي تمثل العقد الذي يجب على كل تنفيذ الوفاء به. تعتمد على plugin_platform_interface لضمان أن التنفيذات المعتمدة فقط يمكن تعيينها لـ instance.

حزمة واجهة المنصة — lib/my_battery_info_platform_interface.dart

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

/// الواجهة التي يجب على جميع تنفيذات my_battery_info توسيعها.
///
/// يجب على تنفيذات المنصة توسيع هذه الفئة لا تنفيذها،
/// إذ أن [instance] لا يُطبّق واجهة Dart الكاملة.
abstract class MyBatteryInfoPlatform extends PlatformInterface {
  MyBatteryInfoPlatform() : super(token: _token);

  static final Object _token = Object();

  static MyBatteryInfoPlatform _instance = _MethodChannelMyBatteryInfo();

  /// المثيل الافتراضي لـ [MyBatteryInfoPlatform] للاستخدام.
  static MyBatteryInfoPlatform get instance => _instance;

  /// تجاوز لتوفير تنفيذ مخصص (يُستخدم في الاختبارات أو
  /// حزم المنصة التي تُسجّل نفسها).
  static set instance(MyBatteryInfoPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  /// يُعيد مستوى البطارية الحالي (0–100).
  Future<int> getBatteryLevel() {
    throw UnimplementedError('getBatteryLevel() has not been implemented.');
  }

  /// يُعيد ما إذا كان الجهاز يتشحن حالياً.
  Future<bool> isCharging() {
    throw UnimplementedError('isCharging() has not been implemented.');
  }

  /// تدفق من تحديثات مستوى البطارية.
  Stream<int> get batteryLevelStream {
    throw UnimplementedError('batteryLevelStream has not been implemented.');
  }
}
تحذير: لا تدع الحزمة الموجهة للتطبيق تتواصل مع الكود الأصلي مباشرة عبر MethodChannel. توجّه دائماً عبر واجهة المنصة. تجاوز الواجهة يكسر البنية المتحدة ويجعل من المستحيل على فرق المنصة تبادل التنفيذات.

ربط pubspec.yaml

كل حزمة في الاتحاد تمتلك pubspec.yaml يُعلن عن دورها. تستخدم الحزمة الموجهة للتطبيق مفتاح flutter.plugin.platforms لإخبار Flutter بحزم التنفيذ التي تستخدمها على كل منصة:

  • الحزمة الموجهة للتطبيق: تُدرج flutter > plugin > platforms مشيرةً إلى حزم التنفيذ لكل منصة.
  • واجهة المنصة: حزمة Dart عادية؛ لا تحتاج مفتاح flutter.plugin.
  • حزم التنفيذ: تُعلن implements: my_battery_info وdartPluginClass/pluginClass لمنصتها.

ملخص

إنشاء هيكل مكوّن Flutter متحد ينتج ثلاث طبقات منطقية: الحزمة الموجهة للتطبيق بواجهة Dart نظيفة، حزمة واجهة المنصة بالعقد المجرد، وحزمة تنفيذ منصة واحدة أو أكثر تحتوي على الكود الأصلي. يجب أن تكون طبقة Dart API رفيعة ومكتوبة بقوة وموثقة جيداً، وتُفوّض كل العمل إلى PlatformInterface.instance. هذا الهيكل يُعظّم إعادة استخدام الكود، ويسمح بمساهمات منصة مستقلة، ويحافظ على استقرار واجهتك العامة عبر التغييرات الأصلية.