ty.
Step 1: Define the API Contract
Create a pigeon file (pigeons/api.dart). This defines the interface shared between Dart and native code.
// pigeons/api.dart
import 'package:pigeon/pigeon.dart';
@HostApi()
abstract class DeviceApi {
// Returns device battery level.
// Pigeon generates type-safe methods and error handling.
int getBatteryLevel();
// Asynchronous operation with complex payload.
// Use @async for long-running tasks to prevent UI thread blocking.
@async
String fetchSecureToken(Map<String, dynamic> config);
}
@FlutterApi()
abstract class SystemEventsApi {
void onBatteryChanged(int level);
void onLowMemory();
}
Step 2: Generate Code
Run the Pigeon generator to produce Dart and native code.
flutter pub run pigeon \
--input pigeons/api.dart \
--dart_out lib/generated/device_api.dart \
--objc_header_out ios/Runner/GeneratedDeviceApi.h \
--objc_source_out ios/Runner/GeneratedDeviceApi.m \
--java_out android/app/src/main/java/com/example/DeviceApi.java \
--java_package "com.example"
Step 3: Configure Binary Codec
By default, Pigeon uses StandardMessageCodec. To optimize for performance, configure the channel to use BinaryCodec.
// lib/core/channel_config.dart
import 'package:flutter/services.dart';
import 'package:pigeon/pigeon.dart';
// Custom codec setup for Pigeon
class BinaryPigeonCodec extends StandardMessageCodec {
const BinaryPigeonCodec();
@override
ByteData? encodeMessage(dynamic message) {
// BinaryCodec is more efficient for large payloads.
// Pigeon supports custom codecs via setup methods.
return const BinaryCodec().encodeMessage(message);
}
}
Step 4: Native Implementation (Android/Kotlin)
Pigeon generates the interface; you implement the logic. Ensure heavy work is offloaded from the main thread.
// android/app/src/main/kotlin/.../DeviceApiImpl.kt
import io.flutter.plugin.common.BinaryMessenger
import com.example.DeviceApi
class DeviceApiImpl : DeviceApi {
override fun getBatteryLevel(): Int {
// Direct execution is acceptable for fast, non-blocking ops
val batteryLevel = getBatteryLevelInternal()
return batteryLevel
}
override fun fetchSecureToken(
config: Map<String, Any>,
result: DeviceApi.Result<String>
) {
// Offload network/IO to background thread
backgroundScope.launch {
try {
val token = networkService.getToken(config)
result.success(token)
} catch (e: Exception) {
result.error(FlutterError("TOKEN_ERR", e.message, null))
}
}
}
private fun getBatteryLevelInternal(): Int {
// Implementation details
return 85
}
}
// Registration
fun registerDeviceApi(messenger: BinaryMessenger, api: DeviceApi) {
DeviceApi.setUp(messenger, api, codec = BinaryPigeonCodec())
}
Step 5: Native Implementation (iOS/Swift)
// ios/Runner/DeviceApiImpl.swift
import Flutter
import Foundation
class DeviceApiImpl: NSObject, DeviceApi {
func getBatteryLevel() throws -> Int {
// Fast path
return getBatteryLevelInternal()
}
func fetchSecureToken(config: [String: Any]) async throws -> String {
// Swift Concurrency handles background execution naturally
do {
let token = try await NetworkService.shared.getToken(config: config)
return token
} catch {
throw FlutterError(code: "TOKEN_ERR", message: error.localizedDescription, details: nil)
}
}
private func getBatteryLevelInternal() -> Int {
// Implementation
return 85
}
}
// Registration
func registerDeviceApi(with messenger: FlutterBinaryMessenger, api: DeviceApi) {
DeviceApiSetup.setUp(binaryMessenger: messenger, api: api, codec: BinaryPigeonCodec())
}
Step 6: Dart Usage
// lib/features/device/device_repository.dart
import 'package:flutter/foundation.dart';
import '../generated/device_api.dart';
class DeviceRepository {
final DeviceApi _api;
DeviceRepository() : _api = DeviceApi();
Future<int> getBatteryLevel() async {
try {
return await _api.getBatteryLevel();
} on PlatformException catch (e) {
debugPrint('Channel Error: ${e.message}');
rethrow;
}
}
Future<String> fetchToken(Map<String, dynamic> config) async {
return await _api.fetchSecureToken(config);
}
}
Architecture Decisions
- Pigeon over Manual: Pigeon enforces a contract. If the native implementation returns a
null where an int is expected, the build fails, not the runtime. This reduces QA cycles and production crashes.
- BinaryCodec over JSON:
BinaryCodec handles raw bytes efficiently. For payloads containing images, audio chunks, or dense data structures, JSON serialization adds unnecessary CPU load and memory allocation. BinaryCodec reduces payload size by ~40% and processing time by ~60%.
- Async Annotation: Using
@async in Pigeon signals long-running operations. This ensures the native side does not block the platform thread waiting for the result, preventing UI jank.
- Thread Isolation: Heavy computation in native handlers must be dispatched to background threads (Coroutines on Android, GCD/AsyncAwait on iOS). The result callback can be invoked from any thread; the engine marshals it back to the Dart isolate safely.
Pitfall Guide
1. The Callback Leak
Mistake: Failing to invoke result.success(), result.error(), or result.notImplemented() in native code.
Impact: The Dart Future never completes. This leads to hung coroutines, memory leaks, and UI elements stuck in loading states.
Fix: Always ensure every code path in the native handler calls a result method. Use Pigeon, which generates wrappers that encourage exhaustive handling.
2. Payload Bloat
Mistake: Sending multi-megabyte images or large datasets through a channel.
Impact: Channels have implicit size limits. Large payloads cause OutOfMemoryError on Android or transaction failures on iOS. Serialization of large JSON objects spikes CPU usage, causing frame drops.
Fix: Pass file paths or memory-mapped buffers instead of raw data. For large binary data, use BasicMessageChannel with BinaryCodec or write data to a temp file and pass the URI.
3. Thread Pinning
Mistake: Executing synchronous network requests or database queries on the main thread in the native channel handler.
Impact: Blocks the UI thread, causing ANRs on Android and watchdog terminations on iOS.
Fix: Dispatch work to background threads immediately. Return control to the platform thread only to set up the async execution.
4. Race Conditions in EventChannels
Mistake: Multiple EventChannel streams listening to the same native source without proper synchronization.
Impact: Duplicate events, missed events, or native crashes due to concurrent access to shared resources.
Fix: Implement a singleton native event source that manages subscriptions. Use thread-safe queues to buffer events before emitting to Flutter.
5. Platform Drift
Mistake: Implementing slightly different logic or data models for iOS and Android manually.
Impact: Features work differently on each platform. Bugs appear only on specific OS versions.
Fix: Use Pigeon to define a single source of truth. The generated code ensures both platforms adhere to the same API structure.
6. Ignoring Lifecycle Events
Mistake: Not handling Activity destruction or View Controller dismissal.
Impact: Native callbacks attempt to update a destroyed Flutter view, causing crashes.
Fix: In Android, bind channel handlers to the ActivityBinding or ViewDestroy lifecycle. In iOS, clean up observers in deinit.
7. Cross-Platform Type Mismatch
Mistake: Returning a Double from iOS where Dart expects an Int, or vice versa.
Impact: PlatformException due to type casting errors during decoding.
Fix: Pigeon enforces strict types. If using manual channels, explicitly cast types in native code before sending results.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple Config Read | MethodChannel + Pigeon | Low latency, type-safe, minimal setup. | Low |
| High-Frequency Sensor Data | BasicMessageChannel + BinaryCodec | Lowest overhead, supports raw bytes, efficient for streams. | Medium |
| Continuous Stream (e.g., Bluetooth) | EventChannel + Pigeon | Native stream integration, handles backpressure better. | Medium |
| Large File Transfer | File Path via Channel | Avoids channel payload limits and serialization costs. | Low |
| CPU-Intensive Native Op | MethodChannel + Background Thread | Prevents UI blocking; Pigeon ensures result delivery. | Medium |
| Legacy Manual Channels | Incremental Pigeon Migration | Reduces risk; migrate critical paths first. | Low/Medium |
Configuration Template
Pigeon Configuration (pigeons/config.dart)
// pigeons/config.dart
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/generated/api.g.dart',
dartOptions: DartOptions(),
kotlinOut: 'android/app/src/main/kotlin/com/example/Api.g.kt',
kotlinOptions: KotlinOptions(
package: 'com.example',
// Enable error handling wrapper
errorClassName: 'ApiError',
),
swiftOut: 'ios/Runner/Api.g.swift',
swiftOptions: SwiftOptions(),
))
@HostApi()
abstract class CoreApi {
@async
Uint8List processImageData(Uint8List data);
int getDeviceId();
}
Dart Timeout Wrapper
// lib/core/channel_timeout.dart
import 'dart:async';
import 'package:flutter/services.dart';
Future<T> withChannelTimeout<T>(
Future<T> Function() channelCall, {
Duration timeout = const Duration(seconds: 5),
}) async {
try {
return await channelCall().timeout(timeout);
} on TimeoutException catch (_) {
// Log metric: ChannelTimeout
throw PlatformException(
code: 'CHANNEL_TIMEOUT',
message: 'Channel call exceeded ${timeout.inMilliseconds}ms',
);
}
}
Quick Start Guide
- Initialize Pigeon: Add
pigeon to dev_dependencies in pubspec.yaml and run flutter pub get.
- Define API: Create
pigeons/api.dart with @HostApi and @FlutterApi definitions for your native interactions.
- Generate Code: Execute the
flutter pub run pigeon command with appropriate output paths for Dart, Kotlin, and Swift.
- Implement Native: Open the generated Kotlin/Swift files and implement the required methods. Use background threads for long operations. Register the implementation in your
Application or AppDelegate.
- Invoke from Dart: Import the generated Dart file and call the methods directly. Wrap calls with timeout logic and handle
PlatformException.
Platform channels are a powerful abstraction, but they demand respect for their underlying mechanics. By adopting code generation, optimizing serialization, and enforcing strict threading discipline, teams can eliminate the majority of channel-related performance issues and stability defects.