Flutter: قنوات المنصة والتواصل الأصلي
Flutter ليس جزيرة — قنوات المنصة هي الجسر الذي يربطه بالبر الرئيسي الأصلي.
📋 المتطلبات السابقة: يجب أن تكون ملمًا بـ
- الدرس 5: StatefulWidget والتفاعل
1. ما ستتعلمه
- ثلاثة أنواع من Platform Channel: MethodChannel، EventChannel، BasicMessageChannel
- تنفيذ المعالج بـ Kotlin/Java (Android) وSwift (iOS)
- توليد أكواد Pigeon: تواصل ثنائي الاتجاه آمن الأنواع
- سير عمل تطوير الإضافات والهيكل الأساسي flutter-plugin
- ShopApp: استدعاء حزم SDK الأصلية للدفع (تكامل Apple Pay / Google Pay)
2. قصة حقيقية عن تكامل الدفع
(1) المشكلة: Flutter لا يستطيع استدعاء حزم SDK الأصلية للدفع مباشرة
يحتاج ShopApp لبوب لتكامل Apple Pay وGoogle Pay، لكن كلا SDK لهما فقط واجهات API أصلية (Swift/Kotlin) — Flutter لا يستطيع استدعاءها مباشرة. بدون دفع أصلي، معدلات التحويل أقل بـ 40% — المستخدمون لا يريدون كتابة أرقام بطاقات الائتمان يدويًا على الهاتف.
(2) حل MethodChannel
MethodChannel هو قناة رسائل Flutter ↔ أصلية — جانب Flutter يستدعي الطرق، والجانب الأصلي يعالجها ويُرجع النتائج.
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',
});
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(3) الفائدة: تحويل أعلى للدفع الأصلي
بعد تكامل الدفع الأصلي عبر MethodChannel، ارتفع معدل تحويل الدفع لبوب من 55% إلى 92% — Apple Pay/Google Pay يكتمل بنقرة واحدة بدون كتابة.
3. آلية تواصل Platform Channel
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
> الإخراج: شغّل محليًا باستخدام 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
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: جانب Flutter لـ MethodChannel
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;
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: معالج Kotlin لجانب Android
// 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
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: معالج Swift لجانب iOS
// 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
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
5. تواصل الدفق بـ EventChannel
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: مراقبة حالة البطارية
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();
},
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
6. تواصل آمن الأنواع بـ Pigeon
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تعريف وتوليد Pigeon
// 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
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
| ميزة Pigeon | الوصف |
|---|---|
| آمن الأنواع | فحص أنواع المعاملات وقت الترجمة |
| توليد مزدوج المنصة | يُولد أكواد Dart + Kotlin + Swift في وقت واحد |
| أمان القيم الفارغة | الأكواد المُولدة تدعم أمان القيم الفارغة |
| دعم async | توضيح @async يعالج استدعاءات async تلقائيًا |
7. تطوير الإضافات
(1) هيكل مشروع الإضافة
shopapp_payment/
├── lib/
│ └── shopapp_payment.dart # Dart API
├── android/
│ └── src/main/kotlin/ # Android implementation
├── ios/
│ └── Classes/ # iOS implementation
├── pubspec.yaml
└── example/ # Example app
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: إنشاء إضافة
# 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
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
8. مثال كامل: واجهة تكامل الدفع في ShopApp
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)));
}
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
❓ أسئلة شائعة
com.shopapp/payment. يجب أن يكون اسم القناة متسقًا على جانبي Flutter والأصلي.invokeMethod يُرجع Future. المعالجة على الجانب الأصلي تعمل على المسار الرئيسي؛ العمليات الطويلة يجب أن تستخدم مسارًا خلفيًا.📖 ملخص
- MethodChannel يستخدم نمط طلب-استجابة، مناسب للاستدعاءات لمرة واحدة (دفع/كاميرا)
- EventChannel يوفر دفقًا مستمرًا، مناسب للأحداث المستمرة (مستشعرات/موقع)
- الجانب الأصلي Kotlin/Swift يستجيب لاستدعاءات Flutter عبر MethodCallHandler
- توليد أكواد Pigeon يُمكن التواصل ثنائي الاتجاه الآمن الأنواع
- الإضافات تُعبأ كحزم قابلة لإعادة الاستخدام، تُنشر على pub.dev
📝 تمارين
- أساسي (الصعوبة ⭐): أنشئ MethodChannel حيث يستدعي جانب Flutter الكود الأصلي للحصول على طراز الجهاز، ويعيد الجانب الأصلي String.
- متوسط (الصعوبة ⭐⭐): نفّذ EventChannel لمراقبة حالة البطارية الأصلية، مع عرضها فوريًا على جانب Flutter باستخدام StreamBuilder.
- متقدم (الصعوبة ⭐⭐⭐): استخدم Pigeon لتوليد واجهة API للدفع آمنة الأنواع، واستدعِ الدفع الأصلي (محاكى) من Flutter، وتعامل مع نتائج النجاح/الإلغاء/الفشل.