Creating a Flutter Plugin for Native Functionality
You're integrating a proprietary partner SDK—a C++ library with native calls. There are no ready-made plugins on pub.dev, and wrapping via ffi isn't viable due to complex lifecycle management—for example, proper memory handling and synchronization with the Dart isolate. Sound familiar? Then we build a custom plugin using Platform Channels. Our mobile development team, with 5+ years of experience, handles such projects end-to-end: from API design to publishing on pub.dev and integration into your project. We have implemented over 30 custom plugins for clients in fintech, healthcare, and IoT. We cut integration costs by up to 40% by leveraging Pigeon and optimizing platform channels.
How to Choose the Right Platform Channel?
Choosing the correct channel is the foundation of a stable plugin. Let's compare three options:
| Channel | Purpose | Typical Scenario |
|---|---|---|
MethodChannel |
Call a method and get a result (request/response) | Getting OS version, reading a file |
EventChannel |
Stream events from native code to Dart | Sensor data stream, subscribing to BLE notifications |
BasicMessageChannel |
Bidirectional arbitrary data transfer with custom codec | Exchanging complex structures not fitting StandardMessageCodec |
A typical example: a plugin for BLE devices. Device scanning—EventChannel (continuous stream of found devices). Connection/disconnection—MethodChannel. Receiving characteristic notifications—again EventChannel.
Why Pigeon Is Better Than Manual Serialization?
StandardMessageCodec (default) supports primitives, List, Map. For custom objects, we serialize to Map<String, dynamic> on the Dart side and get HashMap on Kotlin / [String: Any] on Swift. But this is prone to key name typos. The alternative—Pigeon: a tool from the Flutter team that generates type-safe APIs from a .dart specification. Pigeon generates Kotlin/Swift code with typed classes, eliminating runtime errors from method name typos. Using Pigeon reduces development time by 30–40% and simplifies maintenance. According to official Flutter documentation, Pigeon is recommended for plugins with complex serialization. More details at Platform Channels.
Compare the two strategies:
| Approach | Type Safety | Development Time | Maintenance |
|---|---|---|---|
| Manual serialization | No (runtime errors) | Longer (tests needed) | Harder (key agreement) |
| Pigeon | Yes (compile-time) | 30–40% faster | Easier (autogeneration) |
How We Develop the Plugin: Steps
- Requirements analysis: which native functions, permissions, lifecycle. We estimate scope: 1–2 methods for one platform, or 10+ with EventChannel for both.
- Dart API design: contract via Pigeon or manual MethodChannel/EventChannel. Determine data types.
-
iOS (Swift) and Android (Kotlin) implementation: use
FlutterPlugin,ActivityAware,MethodCallHandler. Handle all edge cases and errors. - Testing: unit tests on Dart, integration tests on both platforms, testing on real devices.
- Integration into your project: connect via git dependency or publish on pub.dev.
- Documentation: README with API, examples, CHANGELOG.
Common Platform Channel Mistakes
- Double call to
result.success()on Android—IllegalStateException: Reply already submitted. We ensure every call completes exactly once. -
EventSinkleak on screen rotation on Android. Solution: nullifysinkinonCancel()and check before every call. - Method name mismatch between Dart and native code. Pigeon eliminates this.
Plugin Structure
Created via flutter create --template=plugin my_plugin. Structure:
my_plugin/ lib/my_plugin.dart — Dart API android/src/.../MyPlugin.kt — Android implementation ios/Classes/MyPlugin.swift — iOS implementation example/ — example app for testing Dart side declares the contract:
class MyPlugin { static const MethodChannel _channel = MethodChannel('my_plugin'); static Future<String?> getPlatformVersion() async { return await _channel.invokeMethod<String>('getPlatformVersion'); } static Stream<ScanResult> get scanResults { return const EventChannel('my_plugin/scan_results') .receiveBroadcastStream() .map((data) => ScanResult.fromMap(Map<String, dynamic>.from(data))); } } Android Implementation: FlutterPlugin + ActivityAware
On Android, the plugin implements FlutterPlugin for lifecycle, MethodCallHandler for handling calls. If an Activity is needed (e.g., for permission requests), additionally ActivityAware:
class MyPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel private var activity: Activity? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(binding.binaryMessenger, "my_plugin") channel.setMethodCallHandler(this) } override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "getPlatformVersion" -> result.success("Android ${android.os.Build.VERSION.RELEASE}") else -> result.notImplemented() } } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity } } Critical point: result.success(), result.error(), and result.notImplemented() must be called exactly once. Calling result.success() twice crashes with IllegalStateException: Reply already submitted. We guarantee no such errors in our code.
iOS Implementation in Swift
public class MyPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( name: "my_plugin", binaryMessenger: registrar.messenger() ) let instance = MyPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "getPlatformVersion": result("iOS " + UIDevice.current.systemVersion) default: result(FlutterMethodNotImplemented) } } } EventChannel and Memory Leaks
When using EventChannel on Android, the native side receives an EventSink. A typical leak: holding EventSink in a field, Activity recreates on screen rotation, old EventSink is not validated—calling sink.success() after destruction throws an exception. Solution: nullify sink in onCancel() and check before every call.
Publishing and Versioning
For internal use, the plugin lives in a git repository and is connected via path or git dependency in pubspec.yaml. For pub.dev publication, run flutter pub publish with mandatory pubspec.yaml fields (homepage, repository) and a complete CHANGELOG.md.
What's Included in the Work
- Dart API and native implementation for iOS and Android
- Integration with your project (usage example)
- API and build documentation
- Publication on pub.dev or git repository access
- Guarantee of stable platform channels and error handling
Timelines
Development timeline: simple (1–2 methods, one platform)—2–4 days. Full cross-platform plugin with EventChannel, permissions, and edge-case handling—2–4 weeks. Pricing is determined after analysis. Get a consultation: contact us for an assessment of your project. We have already implemented over 30 custom plugins for clients in fintech, healthcare, and IoT—join them. Order plugin development now.







