Skip to content
Boring Doc
Boring Doc explanation Verified against Flutter 3.47.7 3 min readplatform channelsMethodChannelEventChannelKotlinSwift

Platform integration and platform channels

Platform channels, plugins and native code.

A platform channel is a named, asynchronous message pipe between your Dart code and the host platform (Kotlin/Java on Android, Swift/Objective-C on iOS and macOS, C++ on Windows, C on Linux). MethodChannel sends a method name plus arguments and gets a single result back; EventChannel streams events from the host to Dart.

  1. Both sides open a channel with the same unique name (prefix it with a domain, e.g. 'samples.flutter.dev/battery'); Dart calls invokeMethod('name', args) and the host's setMethodCallHandler answers with success, error or notImplemented.
  2. StandardMessageCodec serializes null, bool, int, double, String, typed byte/int/float lists, List and Map automatically; anything else must be flattened into those types first.
  3. On the Dart side, a host error becomes a PlatformException (code, message, details) and a missing handler or unknown method becomes a MissingPluginException, so wrap invokeMethod in try/catch.
  4. EventChannel is the streaming counterpart: the host gets an EventSink in onListen and Dart consumes receiveBroadcastStream() like any Stream.
  5. Threading rules: the host handler runs on the platform's main/UI thread (or a background TaskQueue); the Dart side must call from the root isolate or an isolate registered with BackgroundIsolateBinaryMessenger.
  6. Check pub.dev before writing a channel; for type safety generate the channel code with Pigeon, and for C libraries consider dart:ffi instead of a channel.
Sequence diagramOne MethodChannel round trip

Dart never blocks: invokeMethod returns a Future immediately and the host replies through the binary messenger on the platform main thread.

An interpreter booth between two speakers

Dart and Kotlin (or Swift) do not share a language, so they talk through an interpreter booth: the channel. Each message carries a method name ('what are you asking?') and arguments written in a shared vocabulary that both sides understand: numbers, strings, lists, maps. The host answers through the same booth, and the whole exchange is asynchronous, so the Dart side keeps rendering frames while it waits for the reply.

Where the analogy breaks: A human interpreter can translate anything; the codec only knows a fixed set of types, so custom objects have to be flattened into maps and lists. There is also no compiler checking that both sides agree on method names and argument shapes; a typo only shows up at runtime as a MissingPluginException or a type error. Pigeon exists precisely to generate that contract.

Minimal working example

Dart side: ask the host for the battery level
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class BatteryLevelButton extends StatefulWidget {
  const BatteryLevelButton({super.key});

  @override
  State<BatteryLevelButton> createState() => _BatteryLevelButtonState();
}

class _BatteryLevelButtonState extends State<BatteryLevelButton> {
  static const _platform = MethodChannel('samples.flutter.dev/battery');
  String _status = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    String status;
    try {
      final level = await _platform.invokeMethod<int>('getBatteryLevel');
      status = 'Battery level: $level%.';
    } on PlatformException catch (e) {
      status = 'Failed: ${e.message}';
    } on MissingPluginException {
      status = 'No host implementation on this platform.';
    }
    if (!mounted) return;
    setState(() => _status = status);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ElevatedButton(
          onPressed: _getBatteryLevel,
          child: const Text('Get battery level'),
        ),
        Text(_status),
      ],
    );
  }
}
Expected: On a physical device the text updates to 'Battery level: 87%.'. On the iOS simulator the host reports UNAVAILABLE, so the PlatformException branch runs.Flutter ≥ 3.0.0Platform channels guide
  1. L12The channel name is the contract: the host must open a channel with exactly this string. A domain prefix keeps it unique across plugins.
  2. L18invokeMethod<T> returns Future<T?>; the type argument tells Dart how to cast the decoded result.
  3. L20result.error(...) on the host surfaces here as PlatformException with the same code, message and details.
  4. L22Thrown when no handler is registered for the channel or the host replied notImplemented (for example on a platform you have not implemented yet).
  5. L25The reply arrives asynchronously; guard setState with mounted like any other async callback.

Use cases

  • Call an operating-system API that no pub.dev plugin exposes (battery, a vendor SDK, a system setting).
  • Reuse native code that already exists in an add-to-app host (an Android or iOS codebase that embeds a Flutter module).
  • Stream platform events into Dart (connectivity changes, sensor data, broadcast intents) through an EventChannel.
  • Prototype a small native integration before extracting it into a reusable plugin package.

Benefits

  • Full access to platform APIs in their native language with very little boilerplate on either side.
  • Asynchronous by design: the Dart UI thread is never blocked while the host does its work.
  • The same mechanism works on Android, iOS, macOS, Windows and Linux, so a channel-based plugin can grow platform by platform.

Limitations

  • Not type-safe: method names are strings and arguments are dynamic, so mismatches are runtime errors.
  • Every platform needs its own implementation and test matrix; web does not use channels at all (JavaScript interop instead).
  • Only codec-supported types cross the channel; large payloads are copied and serialized, which can be slow for high-frequency data.
  • Threading rules are strict: host handlers run on the platform main thread unless you opt into a TaskQueue, and Dart calls must come from the root isolate or a registered background isolate.

Alternatives and trade-offs

  • A plugin from pub.dev — A maintained package already wraps the API you need (camera, geolocator, shared_preferences); you inherit multi-platform support and bug fixes.
  • Pigeon — You own both sides and want generated, type-safe Dart and host code instead of hand-matched strings and maps.
  • dart:ffi / native assets — You need to call a C-compatible library directly; FFI skips the message codec and the platform main thread round trip entirely.
  • Platform-specific Dart — The difference is only in the UI or logic, not in a native API: branch on defaultTargetPlatform instead of writing native code.
  • Networking from Dart — The data you need is on a server, not on the device; plain HTTP from Dart needs no native code.

Channel or method name does not match on both sides

Problem: Names are plain strings with no compile-time check. A different prefix, a capital letter or a renamed method means the host never sees the call and Dart throws MissingPluginException.

Fix: Define the channel name once per side as a constant, copy it verbatim, and answer unknown methods with notImplemented so the mismatch is obvious in logs.

✗ Mismatched names
dart
// Dart
const channel = MethodChannel('samples.flutter.dev/Battery');
await channel.invokeMethod<int>('getBatteryLevel');

// Kotlin registers "samples.flutter.dev/battery" -> MissingPluginException
✓ One constant, identical on both sides
dart
// Dart
const kBatteryChannel = 'samples.flutter.dev/battery';
const channel = MethodChannel(kBatteryChannel);
await channel.invokeMethod<int>('getBatteryLevel');

// Kotlin: private val CHANNEL = "samples.flutter.dev/battery"

Letting PlatformException escape

Problem: A host error (or a missing implementation on one platform) rejects the Future. Because the call happens outside a Flutter callback, the error is not routed to FlutterError.onError; it goes to PlatformDispatcher.onError or just prints, and the UI silently does nothing.

Fix: Catch PlatformException (and MissingPluginException) at the call site, show a fallback, and only rethrow what you can't handle.

✗ Unhandled rejection
dart
Future<int?> readLevel() {
  return _platform.invokeMethod<int>('getBatteryLevel'); // throws on simulator
}
✓ Handled at the boundary
dart
Future<int?> readLevel() async {
  try {
    return await _platform.invokeMethod<int>('getBatteryLevel');
  } on PlatformException catch (e) {
    debugPrint('Battery unavailable: ${e.code} ${e.message}');
    return null;
  } on MissingPluginException {
    return null; // platform not implemented yet
  }
}

Sending a custom Dart object through the channel

Problem: StandardMessageCodec only encodes null, bool, num, String, typed data lists, List and Map. Passing a model class throws an ArgumentError at encode time on the Dart side.

Fix: Convert models to Map<String, Object?> before invoking (and back on the host), or let Pigeon generate the data classes and the serialization for you.

Calling a channel from a spawned isolate

Problem: A plain Isolate.spawn has no BinaryMessenger, so invokeMethod fails because the isolate is not connected to the engine.

Fix: Pass RootIsolateToken.instance to the new isolate and call BackgroundIsolateBinaryMessenger.ensureInitialized(token) before touching any channel or plugin.

This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.

  • Writing custom platform-specific code Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/platform-integration/platform-channels
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Platform integration overview Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/platform-integration
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • https://api.flutter.dev/flutter/services/MethodChannel-class.html
    Last checked
    Oct 21, 2026
  • https://api.flutter.dev/flutter/services/EventChannel-class.html
    Last checked
    Oct 21, 2026
Was this useful?
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.