はじめに
「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の第一歩です。