EventChannel: Streaming Native Events to Dart
EventChannel: Streaming Native Events to Dart
EventChannel is Flutter's mechanism for creating a continuous, asynchronous stream of data flowing from native platform code (Android/iOS) into Dart. Unlike MethodChannel, which is request–response (one call, one result), an EventChannel models a broadcast stream: the native side pushes events whenever they occur, and the Dart side reacts via a standard Stream subscription. Common real-world uses include sensor readings, connectivity changes, geolocation updates, Bluetooth scan results, and battery-level monitoring.
EventChannel is a one-directional stream — native pushes events to Dart. If you also need Dart to send data to native, combine it with a MethodChannel.How EventChannel Works
The lifecycle has two sides that mirror each other:
- Dart side — creates an
EventChannelwith a unique name and calls.receiveBroadcastStream()to obtain aStream. Subscribing to that stream (listen) signals native to start, and cancelling the subscription signals native to stop. - Native side — registers a
StreamHandler(Android:EventChannel.StreamHandler; iOS:FlutterStreamHandler). The handler'sonListencallback receives anEventSink(Android) /FlutterEventSink(iOS) which is used to push events. TheonCancelcallback tears down any platform resources.
com.example.myapp/battery.Dart-Side Implementation
On the Dart side you create one EventChannel instance, expose the stream, and subscribe to it inside a StatefulWidget so you can cancel the subscription when the widget is disposed:
Dart — battery level stream
import 'package:flutter/services.dart';
// 1. Declare the channel (same name as native)
const EventChannel _batteryChannel =
EventChannel('com.example.myapp/battery');
class BatteryPage extends StatefulWidget {
const BatteryPage({super.key});
@override
State<BatteryPage> createState() => _BatteryPageState();
}
class _BatteryPageState extends State<BatteryPage> {
// 2. Hold the subscription so we can cancel it
StreamSubscription<dynamic>? _subscription;
String _batteryLevel = 'Unknown';
@override
void initState() {
super.initState();
// 3. Subscribe — this triggers onListen on the native side
_subscription = _batteryChannel
.receiveBroadcastStream()
.listen(
(dynamic event) {
setState(() {
_batteryLevel = '$event%';
});
},
onError: (dynamic error) {
setState(() {
_batteryLevel = 'Error: ${(error as PlatformException).message}';
});
},
);
}
@override
void dispose() {
// 4. Cancel — this triggers onCancel on the native side
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Battery Monitor')),
body: Center(
child: Text(
'Battery: $_batteryLevel',
style: Theme.of(context).textTheme.headlineMedium,
),
),
);
}
}
Android (Kotlin) StreamHandler Setup
In your MainActivity.kt, register the channel and implement EventChannel.StreamHandler. The onListen method starts your native event source and stores the EventSink. The onCancel method cleans up resources.
Android Kotlin — battery StreamHandler
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.os.BatteryManager
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.myapp/battery"
private var eventSink: EventChannel.EventSink? = null
private var batteryReceiver: BroadcastReceiver? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
EventChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, sink: EventChannel.EventSink) {
// Store the sink for later use
eventSink = sink
// Register the broadcast receiver
batteryReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
val level = intent?.getIntExtra(
BatteryManager.EXTRA_LEVEL, -1
) ?: -1
if (level == -1) {
sink.error("UNAVAILABLE", "Battery level unavailable", null)
} else {
sink.success(level)
}
}
}
registerReceiver(batteryReceiver,
IntentFilter(Intent.ACTION_BATTERY_CHANGED))
}
override fun onCancel(arguments: Any?) {
// Unregister to avoid memory leaks
unregisterReceiver(batteryReceiver)
batteryReceiver = null
eventSink = null
}
})
}
}
iOS (Swift) StreamHandler Setup
In AppDelegate.swift, register the channel with a FlutterEventChannel and implement FlutterStreamHandler. Use a Timer or system API to push events through the stored FlutterEventSink.
iOS Swift — battery StreamHandler
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private let channelName = "com.example.myapp/battery"
private var eventSink: FlutterEventSink?
private var timer: Timer?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterEventChannel(
name: channelName,
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setStreamHandler(self)
UIDevice.current.isBatteryMonitoringEnabled = true
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
extension AppDelegate: FlutterStreamHandler {
func onListen(withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// Poll battery every 10 seconds
timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in
let level = Int(UIDevice.current.batteryLevel * 100)
self?.eventSink?(level)
}
// Send immediately
eventSink?(Int(UIDevice.current.batteryLevel * 100))
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
timer?.invalidate()
timer = nil
eventSink = nil
return nil
}
}
Stream Cancellation and Resource Management
Proper teardown is critical to avoid memory leaks and battery drain. Follow these rules:
- Always store the
StreamSubscriptionreturned bylisten()and callcancel()indispose(). - On Android, unregister receivers, stop location updates, or cancel timers inside
onCancel. - On iOS, invalidate timers, remove observers, and nil out the
eventSinkreference inonCancel. - Never call
sink.success()afteronCancelhas fired — doing so crashes the app.
EventSink / FlutterEventSink after onCancel is called will throw an exception. Always null-check or set the sink reference to null/nil in your cancel handler.Passing Arguments to the Stream
You can pass configuration arguments from Dart to the native onListen callback via receiveBroadcastStream(arguments):
Dart — passing arguments to onListen
// Dart: pass a refresh interval argument
_subscription = _sensorChannel
.receiveBroadcastStream({'intervalMs': 500})
.listen((dynamic event) {
// handle event
});
// Kotlin onListen receives it as a Map
override fun onListen(arguments: Any?, sink: EventChannel.EventSink) {
val args = arguments as? Map<*, *>
val intervalMs = (args?.get("intervalMs") as? Int) ?: 1000
// use intervalMs to configure your sensor polling rate
}
Summary
EventChannel enables a native-to-Dart push stream for continuous events. The Dart side calls receiveBroadcastStream().listen() to start and cancels the subscription to stop. Android uses EventChannel.StreamHandler with an EventSink; iOS uses FlutterStreamHandler with a FlutterEventSink. Always clean up native resources in onCancel and null the sink reference to prevent post-cancel crashes. Optional arguments passed via receiveBroadcastStream(args) let you configure the native stream on a per-subscription basis.