Answer
Overview
Flutter uses Platform Channels to communicate between Dart and native code (Kotlin/Java for Android, Swift/Objective-C for iOS). There are two main types: MethodChannel and EventChannel.
MethodChannel
Purpose: Two-way communication for one-time method calls (request-response pattern).
Use cases:
- Call native APIs (camera, GPS, sensors)
- Execute platform-specific code
- Get device information
- Trigger native functions
How It Works
textFlutter (Dart) → MethodChannel → Native (Kotlin/Swift) → Response → Flutter
Dart Side (Flutter)
dartimport 'package:flutter/services.dart'; class BatteryService { // Create channel with unique name static const platform = MethodChannel('com.example.app/battery'); // Call native method Future<int> getBatteryLevel() async { try { final int result = await platform.invokeMethod('getBatteryLevel'); return result; } on PlatformException catch (e) { print("Failed to get battery level: '${e.message}'."); return -1; } } // Call with arguments Future<String> getDeviceInfo(String key) async { final String result = await platform.invokeMethod('getDeviceInfo', {'key': key}); return result; } }
Android Side (Kotlin)
kotlin// MainActivity.kt import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import android.os.BatteryManager import android.content.Context class MainActivity: FlutterActivity() { private val CHANNEL = "com.example.app/battery" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "getBatteryLevel" -> { val batteryLevel = getBatteryLevel() if (batteryLevel != -1) { result.success(batteryLevel) } else { result.error("UNAVAILABLE", "Battery level not available.", null) } } "getDeviceInfo" -> { val key = call.argument<String>("key") result.success("Info for $key") } else -> result.notImplemented() } } } private fun getBatteryLevel(): Int { val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) } }
iOS Side (Swift)
swift// AppDelegate.swift import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let batteryChannel = FlutterMethodChannel( name: "com.example.app/battery", binaryMessenger: controller.binaryMessenger ) batteryChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method { case "getBatteryLevel": self?.receiveBatteryLevel(result: result) case "getDeviceInfo": if let args = call.arguments as? [String: Any], let key = args["key"] as? String { result("Info for \(key)") } default: result(FlutterMethodNotImplemented) } }) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } private func receiveBatteryLevel(result: FlutterResult) { UIDevice.current.isBatteryMonitoringEnabled = true let batteryLevel = Int(UIDevice.current.batteryLevel * 100) if batteryLevel == -1 { result(FlutterError(code: "UNAVAILABLE", message: "Battery level not available", details: nil)) } else { result(batteryLevel) } } }
EventChannel
Purpose: One-way continuous stream of events from native to Dart (stream pattern).
Use cases:
- Listen to sensor data (accelerometer, gyroscope)
- Monitor battery level changes
- Location updates
- Bluetooth/WiFi state changes
- Native broadcast receivers
How It Works
textNative (Kotlin/Swift) → Stream → EventChannel → Flutter (Dart Stream)
Dart Side (Flutter)
dartimport 'package:flutter/services.dart'; class SensorService { // Create event channel static const EventChannel _channel = EventChannel('com.example.app/sensor'); // Listen to continuous stream Stream<double> get accelerometerStream { return _channel.receiveBroadcastStream().map((event) => event as double); } } // Usage in Widget class SensorWidget extends StatefulWidget { _SensorWidgetState createState() => _SensorWidgetState(); } class _SensorWidgetState extends State<SensorWidget> { double _value = 0.0; StreamSubscription? _subscription; void initState() { super.initState(); // Subscribe to stream _subscription = SensorService() .accelerometerStream .listen((value) { setState(() { _value = value; }); }); } void dispose() { _subscription?.cancel(); super.dispose(); } Widget build(BuildContext context) { return Text('Accelerometer: $_value'); } }
Android Side (Kotlin)
kotlinimport io.flutter.plugin.common.EventChannel import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager class MainActivity: FlutterActivity() { private val CHANNEL = "com.example.app/sensor" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) EventChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) .setStreamHandler(object : EventChannel.StreamHandler { private var sensorManager: SensorManager? = null private var sensor: Sensor? = null private var listener: SensorEventListener? = null override fun onListen(arguments: Any?, events: EventChannel.EventSink) { sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) listener = object : SensorEventListener { override fun onSensorChanged(event: SensorEvent) { // Send data to Flutter continuously events.success(event.values[0].toDouble()) } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} } sensorManager?.registerListener( listener, sensor, SensorManager.SENSOR_DELAY_NORMAL ) } override fun onCancel(arguments: Any?) { sensorManager?.unregisterListener(listener) listener = null } }) } }
iOS Side (Swift)
swiftimport CoreMotion class AppDelegate: FlutterAppDelegate { private var motionManager: CMMotionManager? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController let eventChannel = FlutterEventChannel( name: "com.example.app/sensor", binaryMessenger: controller.binaryMessenger ) eventChannel.setStreamHandler(AccelerometerStreamHandler()) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } class AccelerometerStreamHandler: NSObject, FlutterStreamHandler { private var motionManager: CMMotionManager? func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { motionManager = CMMotionManager() motionManager?.startAccelerometerUpdates(to: .main) { (data, error) in if let data = data { events(data.acceleration.x) } } return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { motionManager?.stopAccelerometerUpdates() motionManager = nil return nil } }
Comparison Table
| Feature | MethodChannel | EventChannel |
|---|---|---|
| Communication | Two-way (request-response) | One-way (native → Dart) |
| Pattern | Single call | Continuous stream |
| Use case | Get battery level, call API | Listen to sensors, location |
| Dart API | text | text |
| Returns | text | text |
| Handler | text | text |
| Example | Get device info once | Monitor battery continuously |
When to Use Which?
Use MethodChannel When:
- You need a one-time response
- Making a function call to native code
- Examples: Get battery level, open settings, take photo
Use EventChannel When:
- You need continuous updates
- Listening to streams of data
- Examples: Sensor data, location updates, WiFi changes
Combined Example
dartclass DeviceService { // MethodChannel - get device name once static const _methodChannel = MethodChannel('com.example/device'); // EventChannel - listen to battery changes static const _eventChannel = EventChannel('com.example/battery'); Future<String> getDeviceName() async { return await _methodChannel.invokeMethod('getDeviceName'); } Stream<int> get batteryStream { return _eventChannel .receiveBroadcastStream() .map((dynamic event) => event as int); } }
Key Differences Summary
MethodChannel: Like a phone call - you call, get answer, done.
EventChannel: Like a radio broadcast - continuously receiving updates.