Wednesday, March 27, 2024

FlutterのMethod ChannelとEvent Channel: ネイティブ連携 完全ガイド (Android/iOS実践編)

Flutterのプラットフォームチャネル入門: Method ChannelとEvent Channel

Flutterは、Dartコードとプラットフォーム固有のネイティブコード(AndroidではKotlin/Java、iOSではSwift/Objective-C)間の通信を可能にする堅牢なメカニズムを提供しています。これらの中でも、MethodChannelEventChannelは、プラグインを構築したり、Flutterが直接公開していないネイティブ機能にアクセスしたりする際に基本となるものです。

Method Channel: リクエスト・レスポンス型の通信

MethodChannelは、Dartとネイティブコード間で非同期のメソッド呼び出しを容易にします。これは、Dartがネイティブ関数を呼び出し、オプションで単一の結果を受け取る必要がある場合に最適です。Dartがリクエストを送信し、ネイティブ側がレスポンス(またはエラー)を返すという意味で、双方向の通信と捉えることができます。

主な用途:

  • 単一のデータ取得(例: バッテリー残量、デバイス名)
  • ネイティブアクションのトリガー(例: 特定のネイティブUIの表示、サウンドの再生)
  • ネイティブ側での一度きりの計算処理

Dart側の例 (Flutterウィジェット内):


import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // MethodChannelとPlatformExceptionに必要

class BatteryInfoWidget extends StatefulWidget {
  const BatteryInfoWidget({super.key});

  @override
  State createState() => _BatteryInfoWidgetState();
}

class _BatteryInfoWidgetState extends State {
  // 1. チャネルを定義します。名前はネイティブ側と一致させる必要があります。
  static const platform = MethodChannel('samples.flutter.dev/battery');
  String _batteryLevel = 'バッテリー残量不明';

  @override
  void initState() {
    super.initState();
    _getBatteryLevel(); // ウィジェット初期化時にバッテリー残量を取得
  }

  // 2. ネイティブ関数を呼び出す非同期メソッドを定義します。
  Future _getBatteryLevel() async {
    String batteryLevel;
    try {
      // 3. メソッドを呼び出します。'getBatteryLevel'はネイティブ側のメソッド名です。
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'バッテリー残量: $result%';
    } on PlatformException catch (e) {
      batteryLevel = "バッテリー残量の取得に失敗しました: '${e.message}'";
    } catch (e) {
      batteryLevel = "予期せぬエラーが発生しました: '${e.toString()}'";
    }

    if (mounted) { // ウィジェットがまだツリーに存在するか確認
      setState(() {
        _batteryLevel = batteryLevel;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('バッテリー情報')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_batteryLevel),
            ElevatedButton(
              onPressed: _getBatteryLevel,
              child: const Text('バッテリー残量を更新'),
            ),
          ],
        ),
      ),
    );
  }
}

Event Channel: ネイティブからDartへのデータストリーミング

EventChannelは、ネイティブコードからDartへデータをストリーミングするために設計されています。Dartはストリームを購読し、ネイティブ側は時間経過とともに複数のイベント(データパケットやエラー通知)を送信できます。Dartがリスニングを開始しますが、イベントの継続的な流れは通常、ネイティブからDartへの一方向です。

主な用途:

  • 継続的なセンサー更新の受信(例: 加速度センサー、GPS位置情報)
  • ネイティブイベントの監視(例: ネットワーク接続の変更、バッテリー状態の変更)
  • 長時間実行されるネイティブタスクの進捗更新の受信

Dart側の例 (Flutterウィジェット内):


import 'dart:async'; // StreamSubscriptionに必要
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // EventChannelに必要

class ConnectivityMonitorWidget extends StatefulWidget {
  const ConnectivityMonitorWidget({super.key});

  @override
  State createState() => _ConnectivityMonitorWidgetState();
}

class _ConnectivityMonitorWidgetState extends State {
  // 1. チャネルを定義します。名前はネイティブ側と一致させる必要があります。
  static const eventChannel = EventChannel('samples.flutter.dev/connectivity');
  String _connectionStatus = '不明';
  StreamSubscription? _connectivitySubscription;

  @override
  void initState() {
    super.initState();
    _enableEventReceiver();
  }

  void _enableEventReceiver() {
    // 2. EventChannelからのブロードキャストストリームをリッスンします。
    _connectivitySubscription = eventChannel.receiveBroadcastStream().listen(
      _onEvent,
      onError: _onError,
      cancelOnError: true, // エラー発生時に自動的に購読をキャンセル
    );
  }

  void _onEvent(Object? event) { // イベントはコーデックがサポートする任意の型にできます
    if (mounted) {
      setState(() {
        _connectionStatus = event?.toString() ?? 'nullイベントを受信';
      });
    }
  }

  void _onError(Object error) {
    if (mounted) {
      setState(() {
        _connectionStatus = '接続状態の取得に失敗しました: ${error.toString()}';
      });
    }
  }

  @override
  void dispose() {
    // 3. ウィジェットが破棄される際に購読をキャンセルします。
    _connectivitySubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('接続モニター')),
      body: Center(
        child: Text('接続状態: $_connectionStatus'),
      ),
    );
  }
}

Android (Kotlin) でのMethod ChannelとEvent Channelの利用

Androidでプラットフォームチャネルを使用するには、通常、MainActivity.ktまたはカスタムFlutterプラグイン内でハンドラを登録します。

Android (Kotlin) - Method Channelの例 (MainActivity.kt内):


package com.example.my_flutter_app // アプリのパッケージ名に置き換えてください

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

class MainActivity: FlutterActivity() {
    private val BATTERY_CHANNEL_NAME = "samples.flutter.dev/battery" // Dart側と一致させる必要があります

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        // MethodChannelのセットアップ
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL_NAME).setMethodCallHandler {
            call, result ->
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()
                if (batteryLevel != -1) {
                    result.success(batteryLevel)
                } else {
                    result.error("UNAVAILABLE", "バッテリー残量が利用できません。", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getBatteryLevel(): Int {
        val batteryLevel: Int
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }
        return batteryLevel
    }
}

上記のコードは、MainActivityMethodChannelをセットアップします。Flutterが'getBatteryLevel'メソッドを呼び出すと、ネイティブのKotlinコードが現在のバッテリー残量を取得し、成功結果として返すか、利用できない場合はエラーを返します。

Android (Kotlin) - Event Channelの例 (MainActivity.kt内):


package com.example.my_flutter_app // アプリのパッケージ名に置き換えてください

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
// import android.os.Handler // 必要に応じて
// import android.os.Looper // 必要に応じて

class MainActivity: FlutterActivity() {
    private val CONNECTIVITY_CHANNEL_NAME = "samples.flutter.dev/connectivity" // Dart側と一致させる必要があります
    private var connectivityReceiver: BroadcastReceiver? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        // EventChannelのセットアップ
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, CONNECTIVITY_CHANNEL_NAME).setStreamHandler(
            object : EventChannel.StreamHandler {
                override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                    // 初期の接続状態を送信
                    events?.success(checkConnectivity())

                    // 接続変更をリッスンするためのBroadcastReceiverをセットアップ
                    connectivityReceiver = createConnectivityReceiver(events)
                    val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
                    registerReceiver(connectivityReceiver, filter)
                }

                override fun onCancel(arguments: Any?) {
                    unregisterReceiver(connectivityReceiver)
                    connectivityReceiver = null
                }
            }
        )
    }

    private fun createConnectivityReceiver(events: EventChannel.EventSink?): BroadcastReceiver {
        return object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                events?.success(checkConnectivity())
            }
        }
    }

    private fun checkConnectivity(): String {
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val network = connectivityManager.activeNetwork
            val capabilities = connectivityManager.getNetworkCapabilities(network)
            return if (capabilities != null &&
                (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
                 capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
                 capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))) {
                "接続済み"
            } else {
                "切断"
            }
        } else {
            // API 29+では非推奨
            @Suppress("DEPRECATION")
            val activeNetworkInfo = connectivityManager.activeNetworkInfo
            @Suppress("DEPRECATION")
            return if (activeNetworkInfo != null && activeNetworkInfo.isConnected) {
                "接続済み"
            } else {
                "切断"
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // ストリームがアクティブな状態でアクティビティが破棄された場合にレシーバーが登録解除されることを保証
        if (connectivityReceiver != null) {
            unregisterReceiver(connectivityReceiver)
            connectivityReceiver = null
        }
    }
}

このAndroidの例では、EventChannelをセットアップします。Dartがリスニングを開始すると、ネイティブコードは接続変更のためのBroadcastReceiverを登録します。接続が変更されるたびに、イベント(「接続済み」または「切断」)がEventSink経由でFlutterに送信されます。Dartがストリームをキャンセルすると、レシーバーは登録解除されます。

iOS (Swift) でのMethod ChannelとEvent Channelの利用

iOSの場合、通常、AppDelegate.swiftファイルまたはカスタムFlutterプラグイン内でチャネルハンドラを登録します。

iOS (Swift) - Method Channelの例 (AppDelegate.swift内):


import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  private let BATTERY_CHANNEL_NAME = "samples.flutter.dev/battery" // Dart側と一致させる必要があります

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    guard let controller = window?.rootViewController as? FlutterViewController else {
      fatalError("rootViewControllerがFlutterViewController型ではありません")
    }

    // MethodChannelのセットアップ
    let batteryChannel = FlutterMethodChannel(name: BATTERY_CHANNEL_NAME,
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // 注意: このメソッドはUIスレッドで呼び出されます。
      if call.method == "getBatteryLevel" {
        self.receiveBatteryLevel(result: result)
      } else {
        result(FlutterMethodNotImplemented)
      }
    })

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func receiveBatteryLevel(result: FlutterResult) {
    UIDevice.current.isBatteryMonitoringEnabled = true // 重要!
    if UIDevice.current.batteryState == .unknown {
      result(FlutterError(code: "UNAVAILABLE",
                          message: "バッテリー残量が利用できません。",
                          details: nil))
    } else {
      result(Int(UIDevice.current.batteryLevel * 100)) // batteryLevelは0.0から1.0
    }
  }
}

このiOS Swiftの例では、AppDelegateFlutterMethodChannelをセットアップします。Flutterが'getBatteryLevel'を呼び出すと、Swiftコードはバッテリー監視を有効にし、バッテリー残量を取得して返します。バッテリー状態が不明な場合はエラーを返します。

iOS (Swift) - Event Channelの例 (AppDelegate.swift内):

Event Channelの場合、AppDelegate(または専用クラス)がFlutterStreamHandlerに準拠する必要があります。


import UIKit
import Flutter
// 接続性のためには、Reachability.swiftのようなライブラリやNetwork.frameworkを使用することがあります。
// 簡単のため、この例ではイベントをシミュレートします。
// 実際の接続モニターには、NWPathMonitor (iOS 12+) または SCNetworkReachability を使用します。

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler { // FlutterStreamHandlerに準拠
  private let CONNECTIVITY_CHANNEL_NAME = "samples.flutter.dev/connectivity" // Dart側と一致させる必要があります
  private var eventSink: FlutterEventSink?
  // 実際のアプリでは、実際の接続性のためにNWPathMonitorなどを使用します。
  // このタイマーはデモンストレーション用です。
  private var timer: Timer?

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    guard let controller = window?.rootViewController as? FlutterViewController else {
      fatalError("rootViewControllerがFlutterViewController型ではありません")
    }

    // EventChannelのセットアップ
    let connectivityChannel = FlutterEventChannel(name: CONNECTIVITY_CHANNEL_NAME,
                                                  binaryMessenger: controller.binaryMessenger)
    connectivityChannel.setStreamHandler(self) // 'self'がonListenとonCancelを処理します

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  // MARK: - FlutterStreamHandler メソッド

  // Flutterがストリームのリッスンを開始したときに呼び出されます
  public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    self.eventSink = events
    // この例では、定期的に接続状態を送信することをシミュレートします
    // 実際のアプリでは、実際のシステム通知(例: NWPathMonitor)に登録します
    self.eventSink?("接続済み (初期)") // 初期イベントを送信

    // 例: タイマーでネットワーク変更をシミュレート
    self.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
        let isConnected = arc4random_uniform(2) == 0 // ランダムに接続または切断
        self?.eventSink?(isConnected ? "接続済み (シミュレート)" : "切断 (シミュレート)")
    }
    return nil // エラーなし
  }

  // Flutterがストリームのリッスンを停止したときに呼び出されます
  public func onCancel(withArguments arguments: Any?) -> FlutterError? {
    self.eventSink = nil
    self.timer?.invalidate()
    self.timer = nil
    // 実際のアプリでは、ここでシステム通知から登録解除します
    return nil // エラーなし
  }
}

このiOS Swiftの例は、FlutterEventChannelのセットアップを示しています。AppDelegateFlutterStreamHandlerに準拠します。 Dartがリスニングを開始すると(onListen)、FlutterEventSinkを保存し、接続イベントを送信するタイマーを開始します(シミュレーション)。実際のアプリケーションでは、NWPathMonitor(iOS 12+の場合)や他のメカニズムを使用して実際のネットワーク変更を検出し、eventSinkで状態を送信します。 Dartがストリームをキャンセルすると(onCancel)、シンクはクリアされ、タイマーは停止されます(またはネイティブリスナーが削除されます)。

重要な考慮事項とベストプラクティス

  • チャネル名: アプリケーション全体で一意であり、Dart側とネイティブ側で同一である必要があります。一般的な慣習はyour.domain/featureNameです。
  • データ型: プラットフォームチャネルは、基本型(null、ブール値、数値、文字列)、バイト配列、これらのリストおよびマップをサポートする標準メッセージコーデックを使用します。複雑なカスタムオブジェクトの場合は、これらのサポートされている型のいずれか(例: マップやJSON文字列)にシリアライズします。
  • 非同期操作: すべてのチャネル通信は非同期です。Dartではasync/awaitを使用し、ネイティブ側では適切なスレッド処理/コールバックメカニズムを使用します。
  • エラー処理: Dart側では常に潜在的なPlatformExceptionを処理します。ネイティブ側では、MethodChannelの場合はresult.error()、EventChannelの場合はeventSink.error()を使用してエラーをDartに伝播します。
  • ライフサイクル管理:
    • EventChannelの場合、ネイティブ側のStreamHandleronCancelメソッドでネイティブリソース(リスナーやオブザーバーなど)をクリーンアップし、DartのdisposeメソッドでStreamSubscriptionをキャンセルするようにしてください。
    • MethodChannelの場合、特定のウィジェットのライフサイクルに関連付けられている場合は、そのスコープを考慮してください。アプリケーションレベルで登録されたチャネル(MainActivityAppDelegateなど)は、アプリの存続期間中持続します。
  • スレッドセーフティ:
    • ネイティブのメソッドコールハンドラ(MethodChannel用)およびストリームハンドラ(EventChannel用)は、通常、プラットフォームのメインUIスレッドで呼び出されます。
    • ネイティブ側で長時間実行されるタスクを実行する場合は、UIスレッドをブロックしないようにバックグラウンドスレッドにディスパッチします。その後、結果/イベントをFlutterに送り返す前にメインスレッドに切り替える必要があるのは、それらの結果/イベントがネイティブUIコンポーネントと対話する必要がある場合です(ただし、チャネル通信自体については、result.success/errorおよびeventSink.success/errorは一般的にスレッドセーフです)。
  • プラグイン: 再利用可能なプラットフォーム固有の機能については、チャネル実装をFlutterプラグインにパッケージ化します。これにより、モジュール性と共有性が向上します。

Method ChannelとEvent Channelを理解し、正しく実装することで、基盤となるネイティブプラットフォームの能力を最大限に活用し、Flutterアプリケーションの機能を大幅に拡張できます。


0 개의 댓글:

Post a Comment