Создание Flutter-плагина для нативной функциональности
Flutter предоставляет обширный набор пакетов на pub.dev, но рано или поздно встречается функциональность, которой нет ни в одном готовом плагине: проприетарный SDK партнёра, специфичная работа с железом, интеграция с корпоративной системой через нативный агент. Здесь начинается разработка собственного плагина через Platform Channels.
Архитектура Platform Channel
Flutter общается с нативным кодом через MethodChannel, EventChannel и BasicMessageChannel. Выбор зависит от паттерна взаимодействия:
-
MethodChannel— вызов нативного метода и получение результата (request/response) -
EventChannel— поток событий из нативного кода в Dart (стримы, подписки на hardware events) -
BasicMessageChannel— двунаправленная передача произвольных данных с кастомным кодеком
Типичный пример: плагин для работы с BLE-устройством. Сканирование устройств — EventChannel (непрерывный поток найденных устройств). Подключение/отключение — MethodChannel. Получение уведомлений от характеристики — снова EventChannel.
Структура плагина
Создаём через flutter create --template=plugin my_plugin. Структура:
my_plugin/
lib/my_plugin.dart — Dart API
android/src/.../MyPlugin.kt — Android реализация
ios/Classes/MyPlugin.swift — iOS реализация
example/ — пример приложения для тестирования
Dart-сторона объявляет контракт:
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-реализация: FlutterPlugin + ActivityAware
На Android плагин реализует FlutterPlugin для lifecycle, MethodCallHandler для обработки вызовов. Если нужен Activity (например для permission request), дополнительно 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
}
}
Критичная деталь: result.success(), result.error() и result.notImplemented() должны вызываться ровно один раз. Вызов result.success() дважды — краш IllegalStateException: Reply already submitted.
iOS-реализация на 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)
}
}
}
Передача сложных типов
StandardMessageCodec (дефолтный) поддерживает примитивы, List, Map. Для кастомных объектов — сериализуем в Map<String, dynamic> на Dart-стороне и получаем HashMap на Kotlin / [String: Any] на Swift. Альтернатива — Pigeon: инструмент от Flutter team для генерации type-safe API по .dart-спецификации. Pigeon генерирует Kotlin/Swift код с типизированными классами — исключает runtime-ошибки от опечаток в именах методов.
EventChannel и memory leaks
При использовании EventChannel на Android нативный сайд получает EventSink. Типичная утечка: держим EventSink в поле класса, Activity пересоздаётся при повороте экрана, старый EventSink не валидируется — и вызов sink.success() после уничтожения бросает исключение. Решение: обнулять sink в onCancel() и проверять перед каждым вызовом.
Публикация и версионирование
Для внутреннего использования плагин живёт в git-репозитории и подключается через path или git dependency в pubspec.yaml. Для публикации на pub.dev — flutter pub publish с обязательным pubspec.yaml с homepage, repository, полным CHANGELOG.md.
Разработка плагина: простой (1-2 метода, одна платформа) — 2-4 дня. Полноценный cross-platform плагин с EventChannel, permissions и edge-case handling — 2-4 недели. Стоимость рассчитывается индивидуально.







