Question #46MediumFlutter BasicsImportant

Flutter method channel vs event channel

#flutter

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

text
Flutter (Dart) → MethodChannel → Native (Kotlin/Swift) → Response → Flutter

Dart Side (Flutter)

dart
import '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

text
Native (Kotlin/Swift) → Stream → EventChannel → Flutter (Dart Stream)

Dart Side (Flutter)

dart
import '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)

kotlin
import 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)

swift
import 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

FeatureMethodChannelEventChannel
CommunicationTwo-way (request-response)One-way (native → Dart)
PatternSingle callContinuous stream
Use caseGet battery level, call APIListen to sensors, location
Dart API
text
invokeMethod()
text
receiveBroadcastStream()
Returns
text
Future<T>
text
Stream<T>
Handler
text
setMethodCallHandler
text
setStreamHandler
ExampleGet device info onceMonitor 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

dart
class 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.


Resources