Platform channels, plugins and FFI

Flutter · Expert · 16 min

When no plugin exists for a native API or SDK, you have to bridge to Kotlin, Swift or C yourself.

Flutter apps reach native APIs in three ways:

Plugins from pub.dev (camera, url_launcher, shared_preferences...) wrap native code behind a Dart API. Check them first; federated plugins have separate packages per platform.

Platform channels pass asynchronous messages between Dart and the host (Kotlin/Java on Android, Swift/Objective-C on iOS):

  • `MethodChannel` — request/response: Dart calls await channel.invokeMethod<int>('getBattery'); native code registers a handler and replies with a result or an error (which arrives as a PlatformException).
  • `EventChannel` — a stream of events from native to Dart (sensors, connectivity).
  • Messages use the StandardMessageCodec: null, bools, numbers, strings, byte arrays, lists and maps.
  • Native handlers run on the platform's main thread by default; don't block it. Use background task queues for heavy work.

Stringly typed channel names and arguments are error-prone, so the Flutter team's Pigeon package generates type-safe Dart, Kotlin and Swift code from a Dart interface definition.

`dart:ffi` calls C functions directly and synchronously, without message passing — ideal for C libraries (SQLite, image codecs, crypto). package:ffigen generates bindings from headers, and build hooks / native assets can compile and bundle the C code. On iOS and macOS, FFI can also reach Objective-C and Swift via ffigen, and Java/Kotlin via jnigen on Android.

Platform views (AndroidView, UiKitView) embed native UI like maps, at some performance cost.

Example

import 'package:flutter/services.dart';

class BatteryService {
  static const _channel = MethodChannel('com.example.app/battery');

  Future<int?> level() async {
    try {
      return await _channel.invokeMethod<int>('getBatteryLevel');
    } on PlatformException catch (e) {
      // e.code / e.message come from the native side's error reply
      throw StateError('Battery unavailable: ${e.message}');
    } on MissingPluginException {
      return null;          // no handler registered (e.g. running on web)
    }
  }
}

// Android (Kotlin), in MainActivity.configureFlutterEngine:
// MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/battery")
//   .setMethodCallHandler { call, result ->
//     if (call.method == "getBatteryLevel") {
//       val bm = getSystemService(BATTERY_SERVICE) as BatteryManager
//       result.success(bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY))
//     } else result.notImplemented()
//   }

Remember

  • MethodChannel for request/response, EventChannel for native event streams
  • Use Pigeon for type-safe channels instead of raw strings
  • dart:ffi calls C synchronously without channels

Watch out for

  • Doing slow work in a native channel handler on the platform main thread, freezing the app.

Check yourself

  1. When is dart:ffi preferable to a MethodChannel?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/platform-integration/platform-channels

Builds on

Unlocks

Nothing builds on this yet.