Skip to content
ExponaBLOG
FLUTTER · DAY 058 / 100

Platform channels: what every Flutter developer should know

Call native iOS and Android code from Dart.

Expona Admin8 min read

Platform channels: what every Flutter developer should know

At some point, every Flutter project hits a wall: you need to access device hardware, call a legacy native API, or integrate a third-party SDK that has no Flutter wrapper. That's where platform channels come in. Understanding how to bridge Dart and native code is not optional—it's the skill that transforms Flutter from a capable mobile framework into a truly complete development solution.

Platform channels are the communication highways between your Dart code and native iOS or Android implementations. They handle everything from simple one-off requests to continuous streams of sensor data or location updates. In this guide, we'll walk through the two main channel types, show you when and how to use them, and share the patterns we've learned from production systems handling everything from payment processing to real-time vehicle tracking.

Method channels: request-response calls to native code

Method channels are the workhorse of platform communication. Think of them as remote procedure calls: you call a method from Dart, pass arguments, and get a result back. This is synchronous from your perspective—Dart waits for the native layer to respond.

The pattern is straightforward. On the Dart side, you instantiate a MethodChannel with a unique identifier, then call a method by name:

dart
import 'package:flutter/services.dart';

const platform = MethodChannel('com.example.app/native');

Future<String> getDeviceId() async {
  try {
    final String result = await platform.invokeMethod('getDeviceId');
    return result;
  } on PlatformException catch (e) {
    return 'Failed: ${e.message}';
  }
}

On the Android side (Kotlin), you handle that same method name:

dart
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
  private val CHANNEL = "com.example.app/native"

  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
      .setMethodCallHandler { call, result ->
        when (call.method) {
          "getDeviceId" -> {
            val deviceId = android.os.Build.ID
            result.success(deviceId)
          }
          else -> result.notImplemented()
        }
      }
  }
}

And the iOS equivalent (Swift):

dart
import UIKit
import Flutter

@UIApplicationMain
@objc class GeneratedPluginRegistrant: NSObject {}

class ViewController: FlutterViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let controller = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(name: "com.example.app/native",
                                       binaryMessenger: controller.binaryMessenger)
    channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
      switch call.method {
      case "getDeviceId":
        result(UIDevice.current.identifierForVendor?.uuidString)
      default:
        result(FlutterMethodNotImplemented)
      }
    }
  }
}

We use method channels constantly for tasks like querying device sensors, integrating with native payment SDKs, or triggering background services. The key advantage is simplicity: the pattern mirrors traditional API calls that every developer understands.

One real-world example: we built a ride-hailing app that needed to access the device's precise GPS location with high accuracy. The native APIs on iOS and Android differ significantly in how they handle permissions and continuous location updates. We wrapped both implementations behind a single method channel, keeping the Dart code clean while handling platform differences in the native layers.

Event channels: streams of events from native code

Method channels are request-response. But what if the native side needs to push data to Dart repeatedly—sensor readings, location updates, network state changes, or device orientation changes? That's where event channels excel. They provide a unidirectional stream from native to Dart.

On the Dart side, you create an EventChannel and listen to its stream:

dart
import 'package:flutter/services.dart';

const eventChannel = EventChannel('com.example.app/location');

Stream<Map<dynamic, dynamic>> getLocationUpdates() {
  return eventChannel.receiveBroadcastStream().map((dynamic event) {
    return Map<String, dynamic>.from(event);
  });
}

void startListening() {
  getLocationUpdates().listen((location) {
    print('Latitude: ${location['latitude']}, Longitude: ${location['longitude']}');
  });
}

On Android (Kotlin), you implement StreamHandler:

dart
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager

class MainActivity: FlutterActivity() {
  private val CHANNEL = "com.example.app/location"
  private lateinit var locationManager: LocationManager

  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager

    EventChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
      .setStreamHandler(object : EventChannel.StreamHandler {
        override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
          val listener = object : LocationListener {
            override fun onLocationChanged(location: Location) {
              events?.success(mapOf(
                "latitude" to location.latitude,
                "longitude" to location.longitude
              ))
            }
            override fun onProviderEnabled(provider: String) {}
            override fun onProviderDisabled(provider: String) {}
          }
          locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, listener)
        }

        override fun onCancel(arguments: Any?) {
          // Clean up resources
        }
      })
  }
}

And iOS (Swift):

dart
import UIKit
import Flutter
import CoreLocation

class ViewController: FlutterViewController, CLLocationManagerDelegate {
  let locationManager = CLLocationManager()
  var eventSink: FlutterEventSink?

  override func viewDidLoad() {
    super.viewDidLoad()

    let controller = window?.rootViewController as! FlutterViewController
    let eventChannel = FlutterEventChannel(name: "com.example.app/location",
                                            binaryMessenger: controller.binaryMessenger)
    eventChannel.setStreamHandler(self)
  }
}

extension ViewController: FlutterStreamHandler {
  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    self.eventSink = events
    locationManager.delegate = self
    locationManager.requestWhenInUseAuthorization()
    locationManager.startUpdatingLocation()
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    locationManager.stopUpdatingLocation()
    return nil
  }

  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    if let location = locations.last {
      eventSink?([
        "latitude": location.coordinate.latitude,
        "longitude": location.coordinate.longitude
      ])
    }
  }
}

Event channels are essential for long-running, continuous data flows. We've used them extensively for GPS tracking in ride-hailing, sensor fusion in fitness apps, and real-time Bluetooth device communication. The native layer manages the stream lifecycle—starting listeners on onListen and cleaning up on onCancel prevents resource leaks and dangling connections.

When you need platform channels: SDKs without Flutter plugins and platform features

Before diving into building your own platform channels, ask yourself: does a maintained plugin already exist? The Flutter ecosystem is mature, and most common needs are covered.

You'll need platform channels in these scenarios:

  • Proprietary or internal SDKs: A payment processor or analytics vendor provides native SDKs but no Flutter binding. You wrap their APIs.
  • Bleeding-edge platform features: Apple or Google releases a new API before anyone has written a Flutter plugin. You can access it via channels immediately.
  • Complex native integrations: Machine learning inference, advanced camera processing, or hardware-specific functionality that requires deep native knowledge.
  • Legacy codebase: You're integrating Flutter into an existing native iOS or Android app with custom business logic in native code.

A concrete example: we worked on a CRM mobile app that needed to sync encrypted data with an on-premise server using a custom protocol. The encryption library was proprietary and only available as native SDKs. We exposed it via method channels, keeping the cryptography in the native layer where performance matters and the Dart side clean and simple.

Check pub.dev first: most needs already have maintained plugins

Before you write a single line of native code, search pub.dev. The plugin ecosystem is extensive and well-maintained. Popular plugins like geolocator, camera, sensors_plus, firebase_core, and hundreds of others already handle the platform channel boilerplate for you.

Existing plugins come with several advantages:

  • Battle-tested across millions of devices and Android/iOS versions.
  • Actively maintained with security patches and new platform API support.
  • Well-documented with examples and pub.dev scores indicating quality.
  • Follow Flutter plugin conventions, making them familiar to all developers.

Only write your own platform channel integration when no suitable plugin exists or when the existing options don't fit your exact requirements. Even then, consider contributing your wrapper back to the community once it's stable—it benefits everyone.

Common mistakes

Not handling platform exceptions: Native code can fail. Always wrap channel calls in try-catch blocks and surface errors gracefully to users. Unhandled platform exceptions crash your app silently.

Forgetting cleanup on event channels: If you use event channels for resources like location services or Bluetooth connections, implement proper cleanup in onCancel. Forgetting this drains battery and leaves connections open.

Mismatching data types: Dart's type system and native types don't always align. Passing a List<int> to native code that expects a byte array, or vice versa, causes runtime crashes. Test thoroughly across platforms.

Blocking the main thread: Long-running operations on the native side block the UI thread. Use background threads in native code and return results asynchronously. Your Dart code already awaits results, so the native implementation should too.

Hardcoding channel names: Define channel identifiers as constants and reuse them. Typos between Dart and native sides are subtle but fatal. A simple constants file prevents this.

Key takeaways

  • Method channels handle request-response calls to native code; event channels stream continuous data from native to Dart.
  • Channel names must match exactly between Dart and native implementations—define them as constants to avoid typos.
  • Always check pub.dev for existing plugins before writing your own platform channel integration.
  • Wrap method channel calls in try-catch to handle platform exceptions gracefully.
  • Implement proper cleanup in event channel handlers to avoid resource leaks and battery drain.
  • Keep native implementations off the main thread; use background threads and return results asynchronously.
  • Test your channel implementations thoroughly on both iOS and Android—subtle platform differences cause subtle bugs.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles