Flutter: プラットフォームチャネルとネイティブ連携
Flutterは孤島ではない — プラットフォームチャネルがネイティブ本土への架け橋です。
📋 前提条件: 以下に既に慣れている必要があります
- レッスン5:StatefulWidgetとインタラクション
1. このレッスンで学ぶこと
- プラットフォームチャネル3タイプ:MethodChannel、EventChannel、BasicMessageChannel
- Kotlin/Java(Android)とSwift(iOS)のハンドラー実装
- Pigeonコード生成:型安全な双方向通信
- プラグイン開発ワークフローとflutter-pluginスキャフォールディング
- ShopApp:ネイティブ決済SDKの呼び出し(Apple Pay / Google Pay統合)
2. 決済統合のリアルなストーリー
(1) 悩み:Flutterからネイティブ決済SDKを直接呼び出せない
Bobの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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) 成果:ネイティブ決済コンバージョンの向上
MethodChannelでネイティブ決済を統合した後、Bobの決済コンバージョン率は55%から92%に上昇 — Apple Pay/Google Payはワンタップで完了し、入力不要です。
3. プラットフォームチャネル通信メカニズム
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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
: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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Android側Kotlinハンドラー
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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:iOS側Swiftハンドラー
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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
5. EventChannelストリーミング通信
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:バッテリー状態監視
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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
6. Pigeon型安全通信
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Pigeon定義と生成
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
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
| Pigeon機能 | 説明 |
|---|---|
| 型安全 | コンパイル時のパラメータ型チェック |
| デュアルプラットフォーム生成 | Dart + Kotlin + Swiftコードを同時生成 |
| null安全 | 生成コードがnull安全をサポート |
| 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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:プラグインの作成
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`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完全な例:ShopApp決済統合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
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q MethodChannelのチャネル名に命名規則はありますか?
A 逆ドメイン記法を使用してください:
com.shopapp/payment。チャネル名はFlutter側とネイティブ側で一致している必要があります。Q MethodChannelの呼び出しは非同期ですか?
A はい、
invokeMethodはFutureを返します。ネイティブ側の処理はメインスレッドで実行されるため、長時間の処理はバックグラウンドスレッドを使用してください。Q PlatformExceptionはどう処理しますか?
A try-catchでPlatformExceptionをキャッチし、codeフィールドでエラータイプを判別し、ユーザーフレンドリーなメッセージを表示します。
Q Pigeonと手書きMethodChannelのどちらを使うべきですか?
A 新規プロジェクトではPigeonを推奨(型安全、コンパイル時チェック)。シンプルな一回限りの通信にはMethodChannelを直接使っても構いません。
Q プラグインと直接Platform Channelを書くことの違いは何ですか?
A プラグインはpub.devに公開できる再利用可能なパッケージ。直接Platform Channelを書くのはプロジェクト固有の実装です。
Q WebプラットフォームはPlatform Channelをサポートしていますか?
A いいえ。WebではJSインターオプ(dart:js_interop)を使ってブラウザAPIを呼び出します。
📖 まとめ
- MethodChannelはリクエスト・レスポンスパターンを使用し、一回限りの呼び出し(決済/カメラ)に適している
- EventChannelはストリーミングプッシュを提供し、継続的イベント(センサー/位置情報)に適している
- ネイティブ側のKotlin/SwiftはMethodCallHandlerでFlutterの呼び出しに応答
- Pigeonコード生成が型安全な双方向通信を実現
- プラグインは再利用可能なパッケージとしてパッケージ化し、pub.devに公開
📝 練習問題
- 基本 (⭐):MethodChannelを作成し、Flutter側からネイティブコードを呼び出してデバイスモデルを取得し、ネイティブ側がStringを返すようにしてください。
- 中級 (⭐⭐):EventChannelを実装してネイティブのバッテリー状態を監視し、Flutter側でStreamBuilderを使ってリアルタイム表示してください。
- チャレンジ (⭐⭐⭐):Pigeonを使って型安全な決済APIを生成し、Flutterからネイティブ決済(シミュレーション)を呼び出し、成功/キャンセル/失敗の結果を処理してください。