Platform Channels
Irbisa · cheatsheetSeptember 13, 2026

Platform Channels

MethodChannel, EventChannel, plugin development, FFI

Senior Developer20 itemscompressed for a skim
  1. 01

    How do Platform Channels work? Implement a MethodChannel example.

    Hard

    Platform Channels allow Flutter (Dart) to communicate with native code (Android/Kotlin, iOS/Swift).

    // ── FLUTTER SIDE (Dart) ────────────────────────────
    class DeviceService {
      static const _method = MethodChannel('com.example.app/device');
      static const _events = EventChannel('com.example.app/accelerometer');
    
      // One-shot call
    …
  2. 02

    How do you write a Flutter plugin from scratch?

    Hard

    A Flutter plugin is a Dart package that wraps native platform code (Android + iOS) using platform channels.

    // ── Dart API (lib/my_plugin.dart) ─────────────────
    class MyPlugin {
      static const _channel = MethodChannel('com.example/my_plugin');
    
      // Simple method call
      static Future<String> getPlatformVersion() async {
    …
  3. 03

    What is EventChannel and when do you use it instead of MethodChannel?

    Medium

    EventChannel is for a continuous, push-based stream of events from native to Dart.

    // ── DART ───────────────────────────────────────────
    class BatteryWatcher {
      static const _channel = EventChannel('com.example/battery_state');
    
      // Broadcast stream — multiple listeners share one native subscription
      static Stream<int> percent() => _channel
    …
  4. 04

    What is Pigeon and why use it over hand-written MethodChannels?

    Medium

    Pigeon is the official Flutter code-gen tool for type-safe platform channels.

    // ── 1. Define the interface (pigeons/messages.dart) ─
    import 'package:pigeon/pigeon.dart';
    
    @ConfigurePigeon(PigeonOptions(
      dartOut: 'lib/src/messages.g.dart',
      kotlinOut: 'android/src/main/kotlin/com/example/Messages.g.kt',
    …
  5. 05

    How does the threading model work for platform channels, and how do you avoid blocking the UI?

    Hard

    Channels look async to Dart, but the native side runs on the platform thread (main thread on iOS, main thread on Android by default).

    // ── Dart side — invokeMethod is non-blocking, await waits ──
    Future<List<int>> hashLargeFile(String path) async {
      // Don't await this on the build path — show a spinner first
      return await _ch.invokeMethod<List<int>>('hashFile', {'path': path}) ?? [];
    }
    …
  6. 06

    How should you handle errors across a platform channel?

    Medium

    Errors are first-class on channels — both sides have an explicit way to signal failure.

    // ── Dart — typed handling with PlatformException ────
    class CameraService {
      static const _ch = MethodChannel('com.example/camera');
    
      static Future<String> takePicture() async {
        try {
    …
  7. 07

    When should you choose dart:ffi over a MethodChannel?

    Hard

    MethodChannel and FFI both cross the Dart/native boundary, but they solve different problems.

    // ── MethodChannel — appropriate for platform API ──
    class Permissions {
      static const _ch = MethodChannel('com.example/perm');
      static Future<bool> requestCamera() =>
          _ch.invokeMethod<bool>('requestCamera').then((v) => v ?? false);
    }
    …
  8. 08

    How do you call platform code from a background isolate?

    Hard

    Platform channels are bound to a specific BinaryMessenger, which by default lives on the root isolate.

    import 'dart:isolate';
    import 'package:flutter/services.dart';
    
    // 1. Root isolate captures the token and spawns a background isolate
    Future<String> hashViaPlatform(String path) async {
      final token = RootIsolateToken.instance!;
    …
  9. 09

    What is BasicMessageChannel and when does it beat MethodChannel?

    Medium

    BasicMessageChannel is the lowest-level channel: it sends arbitrary messages with no method-call envelope and a configurable codec.

    // ── Dart — peer-to-peer string channel ──────────────
    import 'package:flutter/services.dart';
    
    final _chat = BasicMessageChannel<String>('com.example/chat', StringCodec());
    
    void initChat() {
    …
  10. 10

    A designer wants a native map inside a Flutter screen. How do platform views work and what do they cost?

    Hard

    A platform view puts a real native view inside the widget tree through AndroidView or UiKitView, and the price is that two rendering systems now have to be composited into every frame.

    // ── Dart: create the view and talk to that instance ──
    class NativeMap extends StatelessWidget {
      const NativeMap({super.key, required this.center});
    
      final LatLng center;
    …
  11. 11

    You add a plugin, run the app, and every call throws MissingPluginException. How do you work out why?

    Medium

    The exception means the message reached the platform and nothing there had a handler registered for that channel, so the platform answered "not implemented".

    try {
      final level = await const MethodChannel('com.example/battery').invokeMethod<int>('level');
      return level;
    } on MissingPluginException catch (e) {
      // channel exists in Dart, nothing answered on the platform
      log.warn('no handler for com.example/battery: $e');
    …
  12. 12

    Your Android plugin needs the Activity to request a permission. How do you get it without leaking it?

    Hard

    The Activity arrives through the ActivityAware callbacks, and the rule that keeps it from leaking is that you may hold it only between attach and detach.

    class PermissionPlugin : FlutterPlugin, ActivityAware, MethodCallHandler,
        PluginRegistry.RequestPermissionsResultListener {
    
        private var channel: MethodChannel? = null
        private var activity: Activity? = null           // only between attach/detach
        private var binding: ActivityPluginBinding? = null
    …
  13. 13

    Dart throws a type error casting a channel result to Map<String, dynamic>, and Kotlin crashes casting an id to Int. What is going on?

    Medium

    Both crashes are the same mistake: you asserted a type the standard codec never promised.

    // ── Dart: the codec never promises Map<String, dynamic> ──
    const channel = MethodChannel('com.example/profile');
    
    // ✗ _TypeError at runtime — the platform returned Map<Object?, Object?>
    final bad = await channel.invokeMethod('load') as Map<String, dynamic>;
    …
  14. 14

    Your checkout screen calls a payments plugin. How do you test that code in CI, where there is no device and no native side at all?

    Medium

    Replace the channel's answer rather than the plugin: the test binding lets you install a fake handler on the binary messenger, so invokeMethod returns whatever the test wants.

    void main() {
      TestWidgetsFlutterBinding.ensureInitialized();
    
      const channel = MethodChannel('com.example/payments');
      final messenger =
          TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
    …
  15. 15

    Native decodes a 12-megapixel photo and returns the raw pixels over a MethodChannel; the app stutters and occasionally runs out of memory. What do you change?

    Medium

    Stop moving pixels across the boundary: every channel message is copied, and by default the native handler decodes on the platform thread.

    // ✗ pixels over the channel: copies on both sides, decode on the platform thread
    final pixels = await channel.invokeMethod<Uint8List>('decodePhoto', path);
    final image = await decodeImageFromList(pixels!);      // ~48 MB in flight
    
    // ✓ 1. keep the bytes native, hand Dart a path to an encoded file
    final jpegPath = await channel.invokeMethod<String>('exportJpeg', path);
    …
  16. 16

    An accelerometer EventChannel at 200 Hz makes the UI hitch, and the values lag further behind the longer the screen stays open. Where is the problem?

    Medium

    Every event pays a codec encode, a platform-thread hop and a turn of the Dart event loop, so at 200 Hz you are queueing work faster than the UI isolate drains it.

    class Motion {
      static const _events = EventChannel('com.example/motion');
    
      // ✗ a new stream — and a second native onListen — on every call
      Stream<Sample> get raw => _events.receiveBroadcastStream().map(Sample.from);
    …
  17. 17

    A cold start from a notification tap loses the payload: native calls invokeMethod on the channel and Dart never hears it. Why, and what do you ship instead?

    Hard

    At that moment no Dart handler is registered on the channel, so the message lands in the engine's channel buffer, which holds one message per channel by default and discards the rest with a warning.

    class Push {
      static const _channel = MethodChannel('com.example/push');
    
      // ✓ pull: survives the cold start because native kept the payload
      static Future<PushPayload?> initial() async {
        final map = await _channel.invokeMapMethod<String, Object?>('getInitialMessage');
    …
  18. 18

    A C library reports progress by calling your callback from its own worker thread. How do you deliver that into Dart without crashing the process?

    Hard

    Use NativeCallable.listener — it is the only callback form that may be invoked from a thread the Dart isolate does not own.

    // C side:
    //   typedef void (*progress_cb)(int32_t percent);
    //   void start_job(const char* path, progress_cb cb);   // cb runs on a worker thread
    
    typedef _StartJobC = Void Function(
        Pointer<Utf8>, Pointer<NativeFunction<Void Function(Int32)>>);
    …
  19. 19

    Your FFI plugin loads on Android but DynamicLibrary.open throws on iOS, and in a release build the symbol lookup fails too. What is happening?

    Hard

    Both failures come from the same fact: on Apple platforms your C is usually not a separate library file you can open, and the linker drops anything nothing references.

    const _libName = 'tiles';
    
    final DynamicLibrary _dylib = () {
      if (Platform.isAndroid || Platform.isLinux) {
        // a real file, packaged by Gradle from jniLibs or CMake
        return DynamicLibrary.open('lib$_libName.so');
    …
  20. 20

    Every map tile you send crosses the channel wrapped in a Map and re-boxed field by field. How do you teach the standard codec about your own type?

    Hard

    Subclass the standard codec on every side and give your type a type byte of its own — which is exactly the code Pigeon generates, so writing it by hand is worth it only when Pigeon's shapes do not fit.

    class Tile {
      const Tile(this.z, this.x, this.y, this.bytes);
      final int z, x, y;
      final Uint8List bytes;
    }
    …