Platform Channels
MethodChannel, EventChannel, plugin development, FFI
- 01
How do Platform Channels work? Implement a MethodChannel example.
HardPlatform 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 … - 02
How do you write a Flutter plugin from scratch?
HardA 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 { … - 03
What is EventChannel and when do you use it instead of MethodChannel?
MediumEventChannel 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 … - 04
What is Pigeon and why use it over hand-written MethodChannels?
MediumPigeon 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', … - 05
How does the threading model work for platform channels, and how do you avoid blocking the UI?
HardChannels 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}) ?? []; } … - 06
How should you handle errors across a platform channel?
MediumErrors 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 { … - 07
When should you choose dart:ffi over a MethodChannel?
HardMethodChannel 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); } … - 08
How do you call platform code from a background isolate?
HardPlatform 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!; … - 09
What is BasicMessageChannel and when does it beat MethodChannel?
MediumBasicMessageChannelis 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
A designer wants a native map inside a Flutter screen. How do platform views work and what do they cost?
HardA 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
You add a plugin, run the app, and every call throws MissingPluginException. How do you work out why?
MediumThe 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
Your Android plugin needs the Activity to request a permission. How do you get it without leaking it?
HardThe 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
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?
MediumBoth 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
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?
MediumReplace the channel's answer rather than the plugin: the test binding lets you install a fake handler on the binary messenger, so
invokeMethodreturns whatever the test wants.void main() { TestWidgetsFlutterBinding.ensureInitialized(); const channel = MethodChannel('com.example/payments'); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; … - 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?
MediumStop 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
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?
MediumEvery 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
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?
HardAt 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
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?
HardUse
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
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?
HardBoth 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
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?
HardSubclass 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; } …