Creating Flutter Plugins for Native Functionality
Flutter provides an extensive set of packages on pub.dev, but eventually you encounter functionality no ready plugin offers: a vendor's proprietary SDK, device-specific work, integration with a corporate system via native agent. This is where custom plugin development via Platform Channels begins.
Platform Channel Architecture
Flutter communicates with native code through MethodChannel, EventChannel, and BasicMessageChannel. Choice depends on interaction pattern:
-
MethodChannel—call native method and get result (request/response) -
EventChannel—stream of events from native code to Dart (hardware event subscriptions) -
BasicMessageChannel—bidirectional arbitrary data transfer with custom codec
Typical example: BLE device plugin. Device scanning—EventChannel (continuous stream of discovered devices). Connect/disconnect—MethodChannel. Characteristic notifications—again EventChannel.
Plugin Structure
Create 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, plugin implements FlutterPlugin for lifecycle, MethodCallHandler for call handling. If Activity is needed (permission request), also add 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 detail: result.success(), result.error(), and result.notImplemented() must be called exactly once. Calling result.success() twice causes IllegalStateException: Reply already submitted.
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)
}
}
}
Passing Complex Types
StandardMessageCodec (default) supports primitives, List, Map. For custom objects—serialize to Map<String, dynamic> on Dart side and get HashMap on Kotlin / [String: Any] on Swift. Alternative: Pigeon—a Flutter tool for generating type-safe API from .dart specification. Pigeon generates Kotlin/Swift with typed classes—prevents runtime errors from typos.
EventChannel and Memory Leaks
Using EventChannel on Android, the native side gets EventSink. Typical leak: hold EventSink in a field, Activity recreates on screen rotation, old EventSink isn't validated—calling sink.success() after destruction throws. Solution: nullify sink in onCancel() and check before each call.
Publishing and Versioning
For internal use, plugins live in git and connect via path or git dependency in pubspec.yaml. For pub.dev—flutter pub publish with mandatory pubspec.yaml with homepage, repository, full CHANGELOG.md.
Plugin development: simple (1–2 methods, one platform)—2–4 days. Full cross-platform with EventChannel, permissions, edge-case handling—2–4 weeks. Cost calculated individually.







