Flutter: Platform Channels & Native Interop
Flutter is not an island — Platform Channels are the bridge connecting it to the native mainland.
📋 Prerequisites: You should already be familiar with
- Lesson 5: StatefulWidget & Interaction
1. What You'll Learn
- Platform Channel three types: MethodChannel, EventChannel, BasicMessageChannel
- Kotlin/Java (Android) and Swift (iOS) Handler implementation
- Pigeon code generation: type-safe two-way communication
- Plugin development workflow and flutter-plugin scaffolding
- ShopApp: calling native payment SDKs (Apple Pay / Google Pay integration)
2. A Real Story of Payment Integration
(1) The Pain Point: Flutter Can't Directly Call Native Payment SDKs
Bob's ShopApp needs to integrate Apple Pay and Google Pay, but both SDKs only have native APIs (Swift/Kotlin) — Flutter can't call them directly. Without native payments, conversion rates are 40% lower — users don't want to manually type credit card numbers on mobile.
(2) The MethodChannel Solution
MethodChannel is the Flutter ↔ native message channel — the Flutter side invokes methods, the native side handles them and returns results.
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',
});
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
(3) The Payoff: Higher Native Payment Conversion
After integrating native payments via MethodChannel, Bob's payment conversion rate rose from 55% to 92% — Apple Pay/Google Pay completes in one tap with no typing required.
3. Platform Channel Communication Mechanism
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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
(1) Channel Type Comparison
| Type | Communication Pattern | Data Types | Use Case |
|---|---|---|---|
MethodChannel |
Request-Response | Standard types | One-time calls (payment/camera) |
EventChannel |
Streaming push | Standard types | Continuous events (sensors/GPS) |
BasicMessageChannel |
Two-way messaging | Custom codec | High-frequency bidirectional communication |
(2) Data Type Mapping
| 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 Implementation
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Flutter side 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;
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Android side Kotlin Handler
// 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
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: iOS side Swift Handler
// 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
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
5. EventChannel Streaming Communication
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Battery status monitoring
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();
},
)
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
6. Pigeon Type-Safe Communication
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Pigeon definition and generation
// pigeons/payment_api.dart
import 'package:pigeon/pigeon.dart';
// ⚙️ Dev dependency: 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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
| Pigeon Feature | Description |
|---|---|
| Type safety | Compile-time parameter type checking |
| Dual-platform generation | Generates Dart + Kotlin + Swift code simultaneously |
| Null safety | Generated code supports null safety |
| async support | @async annotation auto-handles async callbacks |
7. Plugin Development
(1) Plugin Project Structure
shopapp_payment/
├── lib/
│ └── shopapp_payment.dart # Dart API
├── android/
│ └── src/main/kotlin/ # Android implementation
├── ios/
│ └── Classes/ # iOS implementation
├── pubspec.yaml
└── example/ # Example app
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Creating a Plugin
# 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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
8. Complete Example: ShopApp Payment Integration UI
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'dart:io';
// ⚙️ Install dependencies: 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)));
}
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
❓ FAQ
com.shopapp/payment. The channel name must be consistent on both the Flutter and native sides.invokeMethod returns a Future. Native-side processing runs on the main thread; long-running operations should use a background thread.📖 Summary
- MethodChannel uses a request-response pattern, suitable for one-time calls (payment/camera)
- EventChannel provides streaming push, suitable for continuous events (sensors/location)
- Native-side Kotlin/Swift responds to Flutter calls via MethodCallHandler
- Pigeon code generation enables type-safe two-way communication
- Plugins are packaged as reusable packages, published to pub.dev
📝 Exercises
- Basic (Difficulty ⭐): Create a MethodChannel where the Flutter side calls native code to get the device model, and the native side returns a String.
- Intermediate (Difficulty ⭐⭐): Implement an EventChannel to monitor native battery status, displaying it in real time on the Flutter side with StreamBuilder.
- Challenge (Difficulty ⭐⭐⭐): Use Pigeon to generate a type-safe payment API, call native payment (simulated) from Flutter, and handle success/cancel/failure results.