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