Flutter: قنوات المنصة والتواصل الأصلي

Flutter ليس جزيرة — قنوات المنصة هي الجسر الذي يربطه بالبر الرئيسي الأصلي.

📋 المتطلبات السابقة: يجب أن تكون ملمًا بـ

1. ما ستتعلمه


2. قصة حقيقية عن تكامل الدفع

(1) المشكلة: Flutter لا يستطيع استدعاء حزم SDK الأصلية للدفع مباشرة

يحتاج ShopApp لبوب لتكامل Apple Pay وGoogle Pay، لكن كلا SDK لهما فقط واجهات API أصلية (Swift/Kotlin) — Flutter لا يستطيع استدعاءها مباشرة. بدون دفع أصلي، معدلات التحويل أقل بـ 40% — المستخدمون لا يريدون كتابة أرقام بطاقات الائتمان يدويًا على الهاتف.

(2) حل MethodChannel

MethodChannel هو قناة رسائل Flutter ↔ أصلية — جانب Flutter يستدعي الطرق، والجانب الأصلي يعالجها ويُرجع النتائج.

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

// Flutter side: invoke native payment
final channel = MethodChannel('com.shopapp/payment');
final result = await channel.invokeMethod<bool>('startPayment', {
  'amount': 99.99,
  'currency': 'USD',
  'merchantId': 'shopapp_inc',
});
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) الفائدة: تحويل أعلى للدفع الأصلي

بعد تكامل الدفع الأصلي عبر MethodChannel، ارتفع معدل تحويل الدفع لبوب من 55% إلى 92% — Apple Pay/Google Pay يكتمل بنقرة واحدة بدون كتابة.


3. آلية تواصل Platform Channel

100%
sequenceDiagram
    participant Flutter
    participant Channel as MethodChannel
    participant Android as Android(Kotlin)
    participant iOS as iOS(Swift)
    Flutter->>Channel: invokeMethod('pay', amount: 99.99)
    Channel->>Android: onMethodCall()
    Channel->>iOS: onMethodCall()
    Android-->>Channel: PaymentResult
    iOS-->>Channel: PaymentResult
    Channel-->>Flutter: result
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(1) مقارنة أنواع القنوات

النوع نمط التواصل أنواع البيانات حالة الاستخدام
MethodChannel طلب-استجابة أنواع قياسية استدعاءات لمرة واحدة (دفع/كاميرا)
EventChannel دفق مستمر أنواع قياسية أحداث مستمرة (مستشعرات/GPS)
BasicMessageChannel رسائل ثنائية الاتجاه مشفر مخصص تواصل ثنائي عالي التردد

(2) تعيين أنواع البيانات

Dart Android (Kotlin) iOS (Swift)
bool Boolean NSNumber(value: Bool)
int Int NSNumber(value: Int)
double Double NSNumber(value: Double)
String String String
List ArrayList NSArray
Map HashMap NSDictionary

4. تنفيذ MethodChannel

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: جانب Flutter لـ MethodChannel

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

// Custom exception class definitions (add to your project):
/*
class PaymentCancelledException implements Exception {}
class PaymentFailedException implements Exception {
  final String message;
  PaymentFailedException(this.message);
}
class PaymentNotSupportedException implements Exception {}
class PaymentException implements Exception {
  final String message;
  PaymentException(this.message);
}
*/

class NativePaymentService {
  static const _channel = MethodChannel('com.shopapp/payment');

  static Future<bool> startPayment({
    required double amount,
    required String currency,
    required String merchantId,
  }) async {
    try {
      final result = await _channel.invokeMethod<bool>('startPayment', {
        'amount': amount,
        'currency': currency,
        'merchant_id': merchantId,
      });
      return result ?? false;
    } on PlatformException catch (e) {
      switch (e.code) {
        case 'PAYMENT_CANCELLED':
          throw PaymentCancelledException();
        case 'PAYMENT_FAILED':
          throw PaymentFailedException(e.message ?? 'Payment failed');
        case 'NOT_SUPPORTED':
          throw PaymentNotSupportedException();
        default:
          throw PaymentException(e.message ?? 'Unknown error');
      }
    }
  }

  static Future<bool> isPaymentAvailable() async {
    return await _channel.invokeMethod<bool>('isPaymentAvailable') ?? false;
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: معالج Kotlin لجانب Android

KOTLIN
// android/app/src/main/kotlin/com/shopapp/MainActivity.kt
class MainActivity : FlutterActivity() {
    private lateinit var channel: MethodChannel

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.shopapp/payment")
        channel.setMethodCallHandler { call, result ->
            when (call.method) {
                "startPayment" -> {
                    val amount = call.argument<Double>("amount") ?: 0.0
                    val currency = call.argument<String>("currency") ?: "USD"
                    val merchantId = call.argument<String>("merchant_id") ?: ""
                    startGooglePay(amount, currency, merchantId, result)
                }
                "isPaymentAvailable" -> {
                    val available = isGooglePayAvailable()
                    result.success(available)
                }
                else -> result.notImplemented()
            }
        }
    }

    private fun startGooglePay(amount: Double, currency: String, merchantId: String, result: MethodChannel.Result) {
        // Google Pay API integration
        val request = PaymentDataRequest.newBuilder()
            .setTransactionInfo(TransactionInfo.newBuilder()
                .setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL)
                .setTotalPrice(amount.toString())
                .setCurrencyCode(currency)
                .build())
            .build()
        // Launch payment activity and return result
    }

    private fun isGooglePayAvailable(): Boolean {
        val isReadyToPayRequest = IsReadyToPayRequest.newBuilder().build()
        // Check Google Pay availability
        return true
    }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: معالج Swift لجانب iOS

SWIFT
// ios/Runner/AppDelegate.swift
import Flutter
import PassKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(_ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let controller = window?.rootViewController as! FlutterViewController
        let channel = FlutterMethodChannel(name: "com.shopapp/payment",
            binaryMessenger: controller.binaryMessenger)

        channel.setMethodCallHandler { (call, result) in
            switch call.method {
            case "startPayment":
                guard let args = call.arguments as? [String: Any],
                      let amount = args["amount"] as? Double,
                      let currency = args["currency"] as? String,
                      let merchantId = args["merchant_id"] as? String else {
                    result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
                    return
                }
                self.startApplePay(amount: amount, currency: currency,
                    merchantId: merchantId, result: result)
            case "isPaymentAvailable":
                result(PKPaymentAuthorizationViewController.canMakePayments())
            default:
                result(FlutterMethodNotImplemented)
            }
        }
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    private func startApplePay(amount: Double, currency: String,
        merchantId: String, result: @escaping FlutterResult) {
        let paymentRequest = PKPaymentRequest()
        paymentRequest.merchantIdentifier = merchantId
        paymentRequest.supportedNetworks = [.visa, .masterCard, .amex]
        paymentRequest.merchantCapabilities = .capability3DS
        paymentRequest.countryCode = "US"
        paymentRequest.currencyCode = currency
        paymentRequest.paymentSummaryItems = [
            PKPaymentSummaryItem(label: "ShopApp", amount: NSDecimalNumber(value: amount))
        ]
        guard let vc = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) else {
            result(FlutterError(code: "NOT_SUPPORTED", message: "Apple Pay not available", details: nil))
            return
        }
        vc.delegate = self
        // Present payment sheet
    }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

5. تواصل الدفق بـ EventChannel

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: مراقبة حالة البطارية

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

// Flutter side
class BatteryService {
  static const _channel = EventChannel('com.shopapp/battery');

  static Stream<double> get batteryLevel {
    return _channel.receiveBroadcastStream().map((event) => event as double);
  }
}

// Usage
StreamBuilder<double>(
  stream: BatteryService.batteryLevel,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text('Battery: ${snapshot.data!.toStringAsFixed(0)}%');
    }
    return const CircularProgressIndicator();
  },
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. تواصل آمن الأنواع بـ Pigeon

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تعريف وتوليد Pigeon

DART
// pigeons/payment_api.dart
import 'package:pigeon/pigeon.dart';

// ⚙️ تبعية تطوير: flutter pub add --dev pigeon

class PaymentRequest {
  double? amount;
  String? currency;
  String? merchantId;
}

class PaymentResult {
  bool? success;
  String? transactionId;
  String? errorMessage;
}

@HostApi()
abstract class PaymentApi {
  @async
  PaymentResult startPayment(PaymentRequest request);
  bool isPaymentAvailable();
}

// Run: dart run pigeon --input pigeons/payment_api.dart
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
ميزة Pigeon الوصف
آمن الأنواع فحص أنواع المعاملات وقت الترجمة
توليد مزدوج المنصة يُولد أكواد Dart + Kotlin + Swift في وقت واحد
أمان القيم الفارغة الأكواد المُولدة تدعم أمان القيم الفارغة
دعم async توضيح @async يعالج استدعاءات async تلقائيًا

7. تطوير الإضافات

(1) هيكل مشروع الإضافة

TEXT
shopapp_payment/
├── lib/
│   └── shopapp_payment.dart    # Dart API
├── android/
│   └── src/main/kotlin/        # Android implementation
├── ios/
│   └── Classes/                # iOS implementation
├── pubspec.yaml
└── example/                    # Example app
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: إنشاء إضافة

BASH
# Create plugin project
flutter create --template=plugin --platforms=android,ios shopapp_payment

# Plugin pubspec.yaml
name: shopapp_payment
description: Native payment integration for ShopApp
version: 1.0.0

flutter:
  plugin:
    platforms:
      android:
        package: com.shopapp.payment
        pluginClass: ShopAppPaymentPlugin
      ios:
        pluginClass: ShopAppPaymentPlugin
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

8. مثال كامل: واجهة تكامل الدفع في ShopApp

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'dart:io';

// ⚙️ تثبيت التبعيات: flutter pub add flutter_riverpod go_router

// Custom class definition source:
// - NativePaymentService: see Section 4 Flutter side MethodChannel in this lesson
// - PaymentCancelledException/PaymentFailedException: see Section 4 in this lesson
// - cartProvider: see Lesson 12 CartNotifier

class PaymentPage extends ConsumerWidget {
  final double total;
  const PaymentPage({super.key, required this.total});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text('Payment')),
      body: FutureBuilder<bool>(
        future: NativePaymentService.isPaymentAvailable(),
        builder: (context, snapshot) {
          final nativeAvailable = snapshot.data ?? false;
          return ListView(children: [
            // Native payment options
            if (nativeAvailable) ...[
              ListTile(
                leading: const Icon(Icons.phone_iphone),
                title: Text(Platform.isIOS ? 'Apple Pay' : 'Google Pay'),
                subtitle: const Text('Fast & secure'),
                trailing: const Icon(Icons.chevron_right),
                onTap: () => _payNative(context, ref),
              ),
              const Divider(),
            ],
            // Credit card option
            ListTile(
              leading: const Icon(Icons.credit_card),
              title: const Text('Credit Card'),
              subtitle: const Text('Visa, Mastercard, Amex'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => context.push('/checkout/card'),
            ),
            // PayPal option
            ListTile(
              leading: const Icon(Icons.account_balance_wallet),
              title: const Text('PayPal'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {},
            ),
            const SizedBox(height: 32),
            Padding(padding: const EdgeInsets.all(16),
              child: Text('Total: \$${total.toStringAsFixed(2)}',
                style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold))),
          ]);
        },
      ),
    );
  }

  Future<void> _payNative(BuildContext context, WidgetRef ref) async {
    try {
      final success = await NativePaymentService.startPayment(
        amount: total, currency: 'USD', merchantId: 'shopapp_inc',
      );
      if (success && context.mounted) {
        ref.read(cartProvider.notifier).clear();
        context.go('/order/confirmed');
      }
    } on PaymentCancelledException {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Payment cancelled')));
    } on PaymentFailedException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(e.message)));
    }
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س هل يوجد اصطلاح تسمية لأسماء قنوات MethodChannel؟
ج استخدم تدوين النطاق المعكوس: com.shopapp/payment. يجب أن يكون اسم القناة متسقًا على جانبي Flutter والأصلي.
س هل استدعاءات MethodChannel غير متزامنة؟
ج نعم، invokeMethod يُرجع Future. المعالجة على الجانب الأصلي تعمل على المسار الرئيسي؛ العمليات الطويلة يجب أن تستخدم مسارًا خلفيًا.
س كيف أتعامل مع PlatformException؟
ج استخدم try-catch لالتقاط PlatformException، ميز أنواع الأخطاء بحقل code، واعرض رسائل ملائمة للمستخدم.
س Pigeon أم MethodChannel مكتوب يدويًا؟
ج يُنصح بـ Pigeon للمشاريع الجديدة (آمن الأنواع، فحص وقت الترجمة)؛ التواصل البسيط لمرة واحدة يمكنه استخدام MethodChannel مباشرة.
س ما الفرق بين الإضافة وكتابة Platform Channels مباشرة؟
ج الإضافة حزمة قابلة لإعادة الاستخدام قابلة للنشر على pub.dev؛ كتابة Platform Channels مباشرة خاصة بالمشروع.
س هل منصة الويب تدعم Platform Channels؟
ج لا. على الويب، استخدم JS interop (dart:js_interop) لاستدعاء واجهات API للمتصفح.

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): أنشئ MethodChannel حيث يستدعي جانب Flutter الكود الأصلي للحصول على طراز الجهاز، ويعيد الجانب الأصلي String.
  2. متوسط (الصعوبة ⭐⭐): نفّذ EventChannel لمراقبة حالة البطارية الأصلية، مع عرضها فوريًا على جانب Flutter باستخدام StreamBuilder.
  3. متقدم (الصعوبة ⭐⭐⭐): استخدم Pigeon لتوليد واجهة API للدفع آمنة الأنواع، واستدعِ الدفع الأصلي (محاكى) من Flutter، وتعامل مع نتائج النجاح/الإلغاء/الفشل.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%