// Flutter · iOS · Android

Flutter Method Channel:
Bridge Swift & Kotlin to Flutter
— Complete Guide

👤 Rohan Kumar Chaudhary
📅 May 2025
12 min read
🏷 Flutter · Swift · Kotlin · Platform Channel
"Flutter is powerful — but the real superpower is when you combine it with native Swift and Kotlin code through Method Channel. That's how Zeno: Between Beats reads real-time health sensor data on both iOS and Android."

Flutter gives you a beautiful cross-platform UI layer — but sometimes you need to go native. Whether it's accessing HealthKit on iOS, reading step count from Google Fit on Android, using a native SDK that has no Flutter plugin, or implementing a platform-specific feature — Method Channel is your bridge.

In this guide I'll cover everything: Method Channel (one-time calls), Event Channel (continuous streams), the Dart side, the Swift side, and the Kotlin side — with real patterns used in production at YenyaSoft.

What Is Method Channel?

Method Channel is Flutter's mechanism for calling native platform code from Dart — and getting a result back. Think of it as an RPC call across the Flutter/native boundary.

Channel type Direction Use case
MethodChannel Dart → Native → Dart (response) One-time calls: get battery level, open camera, fetch device info
EventChannel Native → Dart (stream) Continuous data: heart rate sensor, GPS, accelerometer
BasicMessageChannel Both directions Custom codec, bi-directional messaging

Architecture Overview

Before writing any code, understand the three layers involved:

The channel name is just a unique string — use reverse-domain format: com.yourapp/feature. Both sides must use the exact same string.

Method Channel — Step by Step

Step 1 — Dart side

Create a service class to encapsulate your channel. Never call MethodChannel directly from UI widgets.

health_service.dart
import 'package:flutter/services.dart'; class HealthService { // Channel name must match exactly on iOS and Android static const _channel = MethodChannel('com.zenoapp/health'); // One-time call: get current step count static Future<int> getStepCount() async { try { final result = await _channel.invokeMethod<int>('getStepCount'); return result ?? 0; } on PlatformException catch (e) { debugPrint('getStepCount failed: ${e.message}'); return 0; } } // Call with arguments static Future<Map<String, dynamic>> getHealthData({ required String startDate, required String endDate, }) async { try { final result = await _channel.invokeMethod<Map>( 'getHealthData', {'startDate': startDate, 'endDate': endDate}, ); return Map<String, dynamic>.from(result ?? {}); } on PlatformException catch (e) { throw Exception('Health data unavailable: ${e.message}'); } } }

Step 2 — iOS Swift side

In Xcode, open ios/Runner/AppDelegate.swift. Register the channel and handle method calls.

AppDelegate.swift
import UIKit import Flutter import HealthKit // add to Info.plist: NSHealthShareUsageDescription @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { private let healthStore = HKHealthStore() override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController // Register the channel — same name as Dart let healthChannel = FlutterMethodChannel( name: "com.zenoapp/health", binaryMessenger: controller.binaryMessenger ) healthChannel.setMethodCallHandler { [weak self] call, result in guard let self = self else { return } switch call.method { case "getStepCount": self.fetchStepCount(result: result) case "getHealthData": guard let args = call.arguments as? [String: Any], let startDate = args["startDate"] as? String, let endDate = args["endDate"] as? String else { result(FlutterError( code: "INVALID_ARGS", message: "startDate and endDate required", details: nil )) return } self.fetchHealthData(startDate: startDate, endDate: endDate, result: result) default: result(FlutterMethodNotImplemented) } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } private func fetchStepCount(result: @escaping FlutterResult) { let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)! let query = HKStatisticsQuery( quantityType: stepType, quantitySamplePredicate: HKQuery.predicateForSamplesToday(), options: .cumulativeSum ) { _, stats, _ in let steps = stats?.sumQuantity()?.doubleValue(for: .count()) ?? 0 DispatchQueue.main.async { result(Int(steps)) } } healthStore.execute(query) } private func fetchHealthData(startDate: String, endDate: String, result: @escaping FlutterResult) { // ... parse dates, query HKHealthStore, return as [String: Any] result(["steps": 8420, "calories": 320, "heartRate": 72]) } }

Step 3 — Android Kotlin side

In android/app/src/main/kotlin/.../MainActivity.kt, do the same — same channel name, same method names.

MainActivity.kt
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { private val CHANNEL = "com.zenoapp/health" // same as Dart + Swift override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) .setMethodCallHandler { call, result -> when (call.method) { "getStepCount" -> { val steps = fetchStepCount() result.success(steps) } "getHealthData" -> { val startDate = call.argument<String>("startDate") val endDate = call.argument<String>("endDate") if (startDate == null || endDate == null) { result.error("INVALID_ARGS", "startDate and endDate required", null) } else { result.success(fetchHealthData(startDate, endDate)) } } else -> result.notImplemented() } } } private fun fetchStepCount(): Int { // Query Health Connect / Google Fit SDK here return 6340 } private fun fetchHealthData(startDate: String, endDate: String): Map<String, Any> { return mapOf("steps" to 6340, "calories" to 280, "heartRate" to 68) } }

Event Channel — Continuous Streams

Method Channel is request/response. For real-time sensor data — heart rate, accelerometer, GPS — use EventChannel. It gives you a Dart Stream that pushes native data whenever it arrives.

Dart — listen to the stream

heart_rate_service.dart
class HeartRateService { static const _eventChannel = EventChannel('com.zenoapp/heartrate'); // Returns a broadcast stream of heart rate BPM values static Stream<int> heartRateStream() { return _eventChannel .receiveBroadcastStream() .map((event) => (event as int)); } } // Usage in a BLoC or Cubit: late final StreamSubscription<int> _sub; void startListening() { _sub = HeartRateService.heartRateStream().listen( (bpm) => emit(HeartRateUpdated(bpm)), onError: (e) => emit(HeartRateError(e.toString())), ); }

Swift — push data to Dart

AppDelegate.swift (Event Channel)
var eventSink: FlutterEventSink? // Register in application(_:didFinishLaunchingWithOptions:) let hrChannel = FlutterEventChannel( name: "com.zenoapp/heartrate", binaryMessenger: controller.binaryMessenger ) hrChannel.setStreamHandler(self) // Conform to FlutterStreamHandler extension AppDelegate: FlutterStreamHandler { func onListen( withArguments arguments: Any?, eventSink: @escaping FlutterEventSink ) -> FlutterError? { self.eventSink = eventSink startHeartRateMonitoring() // start your native sensor return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { eventSink = nil stopHeartRateMonitoring() // clean up when Dart unsubscribes return nil } } // Whenever new heart rate data arrives from HKLiveWorkout or CoreMotion: func didReceiveHeartRate(_ bpm: Int) { DispatchQueue.main.async { self.eventSink?(bpm) // push to Dart stream } }

Kotlin — push data to Dart

MainActivity.kt (Event Channel)
import io.flutter.plugin.common.EventChannel private var eventSink: EventChannel.EventSink? = null // In configureFlutterEngine: EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.zenoapp/heartrate") .setStreamHandler(object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink) { eventSink = events startHeartRateMonitoring() // start sensor } override fun onCancel(arguments: Any?) { eventSink = null stopHeartRateMonitoring() // clean up } }) // Push data whenever a new reading arrives: fun onNewHeartRate(bpm: Int) { runOnUiThread { eventSink?.success(bpm) } }

Data Type Mapping

Method Channel automatically converts between Dart, Swift, and Kotlin types. Know this table — wrong types cause silent PlatformException.

Dart Swift Kotlin
null nil null
bool Bool Boolean
int Int Int
double Double Double
String String String
Uint8List FlutterStandardTypedData ByteArray
List [Any] List<Any>
Map [String: Any] HashMap<String, Any>

Best Practices

Common Errors & Fixes

Error Cause Fix
MissingPluginException Channel name mismatch between Dart and native Copy-paste the channel string exactly — no trailing spaces
PlatformException: null Native returned nil/null where non-null expected Use invokeMethod<T?> and handle null in Dart
No data on stream onListen called but native sensor not started Verify startSensor() is inside onListen, not viewDidLoad
UI freezes Native code doing heavy work on main thread Dispatch long operations to background thread, callback on main
type 'int' is not a subtype Native returned Long in Kotlin (64-bit) but Dart expected int Cast to Int in Kotlin before calling result.success()

Production Patterns — Lessons from Zeno

In Zeno: Between Beats, Method Channel bridges HealthKit on iOS and Health Connect on Android to the same Flutter UI. A few patterns that made it maintainable:

Method Channel isn't a workaround — it's the intended architecture for accessing platform capabilities Flutter doesn't expose natively. Master it and you can unlock literally any native SDK from your Flutter app. 🚀

By Rohan Kumar Chaudhary | Flutter · iOS · Mobile Developer 💙
Happy Coding!
#Flutter #MethodChannel #Swift #Kotlin #iOS #Android #FlutterDev #PlatformChannel #EventChannel #MobileDev
Rohan Kumar Chaudhary
Rohan Kumar Chaudhary
Flutter · iOS · Mobile Developer at YenyaSoft