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.
- 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.
- 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.
- 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.
- EventChannel is the streaming counterpart: the host gets an EventSink in onListen and Dart consumes receiveBroadcastStream() like any Stream.
- 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.
- 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.
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.
Minimal working example
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),
],
);
}
}- L12The channel name is the contract: the host must open a channel with exactly this string. A domain prefix keeps it unique across plugins.
- L18invokeMethod<T> returns Future<T?>; the type argument tells Dart how to cast the decoded result.
- L20result.error(...) on the host surfaces here as PlatformException with the same code, message and details.
- L22Thrown when no handler is registered for the channel or the host replied notImplemented (for example on a platform you have not implemented yet).
- 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.
// Dart
const channel = MethodChannel('samples.flutter.dev/Battery');
await channel.invokeMethod<int>('getBatteryLevel');
// Kotlin registers "samples.flutter.dev/battery" -> MissingPluginException// 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.
Future<int?> readLevel() {
return _platform.invokeMethod<int>('getBatteryLevel'); // throws on simulator
}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.
- https://docs.flutter.dev/platform-integration/platform-channels
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/platform-integration
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- MethodChannel class API reference Flutter 3.47.7https://api.flutter.dev/flutter/services/MethodChannel-class.html
- Last checked
- Oct 21, 2026
- EventChannel class API reference Flutter 3.47.7https://api.flutter.dev/flutter/services/EventChannel-class.html
- Last checked
- Oct 21, 2026
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.