0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】Flutter EventChannel完全入門 ── ネイティブのイベントをリアルタイムでFlutterに届ける仕組み

Posted at

はじめに

「MethodChannelが“命令”なら、EventChannelは“息づかい”。」
OSの変化をリアルタイムにFlutterへ伝える魔法、それがEventChannelだ。

Flutterではネイティブ機能(Android/iOS)と通信するためにPlatform Channelsを使います。
その中でも EventChannel は、「継続的に変化するデータをストリームとして受け取る」ための仕組みです。

たとえば:

  • バッテリー残量の変化
  • ネットワーク接続の状態
  • センサーや位置情報
  • Bluetoothの接続・切断イベント

こうした連続するネイティブイベントをDartのStreamとして扱えるのがEventChannelの強みです。


EventChannelの基本構造

Flutter(Dart)
    ↓ receiveBroadcastStream()
EventChannel
    ↓
ネイティブ(Kotlin / Swift)
    ↑
BroadcastReceiver / NotificationCenterなど

Flutterが「リスナー」として待機し、ネイティブがイベントを“push”してくる構造です。


実装例:バッテリー残量の変化を監視

Dart側(Flutter)

import 'package:flutter/services.dart';

class BatteryEvents {
  static const EventChannel _channel =
      EventChannel('system_battery_info/events');

  static Stream<int> get batteryLevelStream =>
      _channel.receiveBroadcastStream().cast<int>();
}

使用例

BatteryEvents.batteryLevelStream.listen((level) {
  print('Battery level changed: $level%');
});

Dart側ではreceiveBroadcastStream()でストリームを購読します。


Android側(Kotlin)

class BatteryEventStreamHandler(private val context: Context)
    : EventChannel.StreamHandler {

    private var eventSink: EventChannel.EventSink? = null
    private var receiver: BroadcastReceiver? = null

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        eventSink = events
        receiver = object : BroadcastReceiver() {
            override fun onReceive(ctx: Context?, intent: Intent?) {
                val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
                if (level != -1) {
                    eventSink?.success(level)
                }
            }
        }
        val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        context.registerReceiver(receiver, filter)
    }

    override fun onCancel(arguments: Any?) {
        context.unregisterReceiver(receiver)
        receiver = null
        eventSink = null
    }
}

FlutterPluginで登録:

class SystemBatteryInfoPlugin : FlutterPlugin {
    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        EventChannel(
            binding.binaryMessenger,
            "system_battery_info/events"
        ).setStreamHandler(BatteryEventStreamHandler(binding.applicationContext))
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {}
}

onListen():Flutterがlisten()したときに呼ばれる
onCancel():ストリーム購読が解除されたときに呼ばれる
eventSink.success():Dartへイベント送信


iOS側(Swift)

public class BatteryEventStreamHandler: NSObject, FlutterStreamHandler {
    private var eventSink: FlutterEventSink?

    public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        eventSink = events
        UIDevice.current.isBatteryMonitoringEnabled = true
        NotificationCenter.default.addObserver(
            forName: UIDevice.batteryLevelDidChangeNotification,
            object: nil,
            queue: nil
        ) { _ in
            let level = Int(UIDevice.current.batteryLevel * 100)
            events(level)
        }
        return nil
    }

    public func onCancel(withArguments arguments: Any?) -> FlutterError? {
        NotificationCenter.default.removeObserver(self)
        eventSink = nil
        return nil
    }
}

登録コード:

public class SystemBatteryInfoPlugin: NSObject, FlutterPlugin {
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterEventChannel(name: "system_battery_info/events",
                                          binaryMessenger: registrar.messenger())
        let instance = BatteryEventStreamHandler()
        channel.setStreamHandler(instance)
    }
}

Flutter側で購読する

StreamSubscription? _sub;

void startListenBattery() {
  _sub = BatteryEvents.batteryLevelStream.listen(
    (level) => print('Battery: $level%'),
    onError: (e) => print('Error: $e'),
  );
}

void stopListenBattery() {
  _sub?.cancel();
}

EventChannelの特徴まとめ

項目 内容
通信方向 ネイティブ → Flutter(ストリーム)
データ型 Stream
利用メソッド receiveBroadcastStream()
ネイティブ側 EventChannel.StreamHandler(Kotlin) / FlutterStreamHandler(Swift)
適用ケース センサー、状態変化、OSイベントなど

よくある落とし穴

問題 原因 対策
イベントが来ない リスナー未登録/isBatteryMonitoringEnabled忘れ onListen()で正しく初期化
メモリリーク unregisterReceiver()忘れ onCancel()で確実に解除
二重登録 listen()を複数回呼ぶ ストリーム共有(asBroadcastStream()など)を使う
スレッドエラー 非メインスレッドでUI更新 Handler(Looper.getMainLooper())で戻す

テストの基本

EventChannelはリアルタイム通信のため、通常はMockストリームでテストします。

test('mock battery stream', () async {
  final controller = StreamController<int>();
  controller.add(90);
  controller.add(85);

  await for (final level in controller.stream) {
    expect(level, lessThanOrEqualTo(100));
  }

  controller.close();
});

通信構造図(Mermaid)


MethodChannelとの使い分け

目的 適したチャネル
単発の呼び出し MethodChannel
継続的な通知 EventChannel
双方向通信(軽量) BasicMessageChannel
高速演算処理 FFI

まとめ

MethodChannel は「命令」を送る。
EventChannel は「変化」を感じる。

Flutterアプリを“反応する存在”にしたいなら、EventChannelは欠かせません。
OSレベルの変化を即座にキャッチし、DartのStreamで反応する──
それがリアクティブFlutterの第一歩です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?