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

1. What You'll Learn


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.

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
> 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

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
> 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

TEXT
> 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

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
> 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

TEXT
> 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

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
> 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

TEXT
> 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

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
> 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

TEXT
> 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

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
> 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

TEXT
> 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

DART
// 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
TEXT
> 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

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
> 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

TEXT
> 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

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
> 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

DART
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)));
    }
  }
}
TEXT
> 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

Q Is there a naming convention for MethodChannel channel names?
A Use reverse domain notation: com.shopapp/payment. The channel name must be consistent on both the Flutter and native sides.
Q Are MethodChannel calls asynchronous?
A Yes, invokeMethod returns a Future. Native-side processing runs on the main thread; long-running operations should use a background thread.
Q How to handle PlatformException?
A Use try-catch to catch PlatformException, distinguish error types by the code field, and display user-friendly messages.
Q Pigeon or hand-written MethodChannel?
A Pigeon is recommended for new projects (type-safe, compile-time checking); simple one-off communication can use MethodChannel directly.
Q What's the difference between a Plugin and directly writing Platform Channels?
A A Plugin is a reusable package publishable to pub.dev; directly writing Platform Channels is project-specific.
Q Does the web platform support Platform Channels?
A No. On web, use JS interop (dart:js_interop) to call browser APIs.

📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Create a MethodChannel where the Flutter side calls native code to get the device model, and the native side returns a String.
  2. Intermediate (Difficulty ⭐⭐): Implement an EventChannel to monitor native battery status, displaying it in real time on the Flutter side with StreamBuilder.
  3. Challenge (Difficulty ⭐⭐⭐): Use Pigeon to generate a type-safe payment API, call native payment (simulated) from Flutter, and handle success/cancel/failure results.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏