Platform Channels Overview & Architecture
Platform Channels Overview & Architecture
Flutter is designed to run on multiple platforms — Android, iOS, web, desktop — with a single Dart codebase. However, not every device capability can be expressed purely in Dart. Features like accessing the device battery level, using Bluetooth, reading NFC tags, or interacting with native SDKs require calling platform-native APIs that are only available in Java/Kotlin on Android or Objective-C/Swift on iOS. Platform channels are Flutter's answer to this problem: a clean, well-defined bridge between the Dart world and the native world.
Why Platform Channels Exist
Flutter's rendering engine (Skia/Impeller) runs entirely in Dart and paints pixels directly on a canvas — bypassing the native view system. This gives Flutter its consistent cross-platform appearance. But it also means Flutter cannot directly call Android or iOS APIs. Platform channels solve this by acting as a message-passing conduit between the Dart isolate and the host platform's main thread, with serialised data crossing the boundary in both directions.
The Three-Layer Architecture
A platform channel integration always involves exactly three layers:
- Dart layer — your Flutter widget or service class that calls the channel and awaits the response.
- Channel layer — a named logical pipe (e.g.
com.example.app/battery) that serialises method names and arguments using a codec, and routes them over the binary messenger. - Native layer — a
MethodChannelhandler registered inMainActivity.kt(Android) orAppDelegate.swift(iOS) that receives the call, executes native code, and sends back a result.
com.yourcompany.appname/feature) to avoid collisions with other channels in large apps or third-party plugins.How Data Flows: The Binary Messenger
All data crossing the platform boundary is serialised by a MessageCodec. Flutter ships with three codecs:
StandardMessageCodec— handlesbool,int,double,String,Uint8List,List, andMap. This is the default.JSONMessageCodec— encodes values as UTF-8 JSON. Less efficient but human-readable.BinaryCodec— passes rawByteDatawith zero encoding overhead; used for high-throughput streaming.
When you invoke a method on a MethodChannel, the Dart runtime hands a byte buffer to the BinaryMessenger, which routes it through the Flutter engine to the platform host. The host decodes the buffer, runs the native code, encodes the return value, and the BinaryMessenger delivers the reply back to the awaiting Dart future.
Dart side — declaring and invoking a MethodChannel
import 'package:flutter/services.dart';
class BatteryService {
// Channel name must match exactly on the native side
static const _channel = MethodChannel('com.example.myapp/battery');
/// Returns the current battery level (0-100) or throws a PlatformException.
Future<int> getBatteryLevel() async {
try {
final int level = await _channel.invokeMethod<int>('getBatteryLevel') ?? -1;
return level;
} on PlatformException catch (e) {
throw Exception('Failed to get battery level: ${e.message}');
}
}
}
Channel Types
Flutter provides three channel classes, each suited to a different communication pattern:
- MethodChannel — request/response. Dart calls a named method and receives exactly one reply. Ideal for one-off queries (battery level, current locale, permission status).
- EventChannel — native-to-Dart stream. The native side pushes a stream of events (sensor readings, network state changes) that Dart consumes as a
Stream. - BasicMessageChannel — free-form bidirectional messaging with a custom codec. Used for arbitrary structured data exchange in either direction.
Minimal example — receiving an EventChannel stream in Dart
import 'package:flutter/services.dart';
class ConnectivityService {
static const _events = EventChannel('com.example.myapp/connectivity');
/// Emits true when connected, false when disconnected.
Stream<bool> get onConnectivityChanged {
return _events
.receiveBroadcastStream()
.map((event) => event as bool);
}
}
// Usage inside a StatefulWidget:
// _subscription = ConnectivityService().onConnectivityChanged.listen(
// (connected) => setState(() => _isOnline = connected),
// );
Threading Model
Understanding the threading model is critical for writing correct platform channel code:
- Dart code runs in a single-threaded Dart isolate. There is no blocking; all channel calls are async.
- On Android, channel handlers are invoked on the main (UI) thread by default. Long-running native operations must be dispatched to a background thread manually.
- On iOS, handlers are invoked on the main thread. The same rule applies — offload heavy work to a background queue.
- The Flutter engine guarantees that replies are delivered back to the Dart isolate safely regardless of which thread sends the reply.
CoroutineScope on Android or DispatchQueue.global() on iOS to run the work asynchronously, then call result.success(value) from any thread.Summary
Platform channels give Flutter a structured, type-safe bridge to native platform APIs without sacrificing the cross-platform consistency that makes Flutter valuable. The key concepts to remember are:
- Three layers: Dart, Channel (codec + messenger), Native handler.
- Three channel types:
MethodChannel(RPC),EventChannel(stream),BasicMessageChannel(free-form). - Data is serialised by a codec; the default (
StandardMessageCodec) handles all primitive Dart types. - All channel calls are asynchronous in Dart; native handlers run on the main thread and must not block.