Flutter: プラットフォームチャネルとネイティブ連携

Flutterは孤島ではない — プラットフォームチャネルがネイティブ本土への架け橋です。

📋 前提条件: 以下に既に慣れている必要があります

1. このレッスンで学ぶこと


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. プラットフォームチャネル通信メカニズム

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`して動作確認してください。実際の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を呼び出します。

📖 まとめ


📝 練習問題

  1. 基本 (⭐):MethodChannelを作成し、Flutter側からネイティブコードを呼び出してデバイスモデルを取得し、ネイティブ側がStringを返すようにしてください。
  2. 中級 (⭐⭐):EventChannelを実装してネイティブのバッテリー状態を監視し、Flutter側でStreamBuilderを使ってリアルタイム表示してください。
  3. チャレンジ (⭐⭐⭐):Pigeonを使って型安全な決済APIを生成し、Flutterからネイティブ決済(シミュレーション)を呼び出し、成功/キャンセル/失敗の結果を処理してください。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%