Создание Flutter-плагина для нативной функциональности
Вы интегрируете проприетарный SDK партнёра — C++ библиотеку с нативными вызовами. Готовых плагинов на pub.dev нет, а обёртка через ffi не подходит из-за сложного lifecycle — например, необходимо правильно управлять памятью и синхронизацией с Dart-изолятом. Знакомая ситуация? Тогда пишем собственный плагин через Platform Channels. Мы, команда мобильных разработчиков с 5+ летним стажем, берём такие задачи под ключ: от проектирования API до публикации на pub.dev и интеграции в ваш проект. У нас за плечами реализация более 30 кастомных плагинов для заказчиков из финтеха, медицины и IoT. Снижаем бюджет на интеграцию до 40% за счёт использования Pigeon и оптимизации платформенных каналов.
Как выбрать тип Platform Channel?
Определение правильного канала — основа стабильной работы плагина. Сравним три варианта:
| Канал | Назначение | Типичный сценарий |
|---|---|---|
MethodChannel |
Вызов метода и получение результата (request/response) | Получение версии ОС, чтение файла |
EventChannel |
Поток событий из нативного кода в Dart | Стрим показаний сенсора, подписка на BLE-уведомления |
BasicMessageChannel |
Двунаправленная передача произвольных данных с кастомным кодеком | Обмен сложными структурами, не влезающими в StandardMessageCodec |
Типичный пример: плагин для работы с BLE-устройством. Сканирование устройств — EventChannel (непрерывный поток найденных устройств). Подключение/отключение — MethodChannel. Получение уведомлений от характеристики — снова EventChannel.
Почему Pigeon лучше ручной сериализации?
StandardMessageCodec (дефолтный) поддерживает примитивы, List, Map. Для кастомных объектов — сериализуем в Map<String, dynamic> на Dart-стороне и получаем HashMap на Kotlin / [String: Any] на Swift. Но это чревато опечатками в именах ключей. Альтернатива — Pigeon: инструмент от Flutter team для генерации type-safe API по .dart-спецификации. Pigeon генерирует Kotlin/Swift код с типизированными классами — исключает runtime-ошибки от опечаток в именах методов. Применение Pigeon сокращает время разработки на 30–40% и упрощает поддержку. Согласно официальной документации Flutter, использование Pigeon рекомендовано для плагинов со сложной сериализацией. Подробнее см. Platform Channels.
Сравните две стратегии:
| Подход | Типобезопасность | Время разработки | Поддержка |
|---|---|---|---|
| Ручная сериализация | Нет (runtime-ошибки) | Больше (нужны тесты) | Сложнее (согласование ключей) |
| Pigeon | Да (compile-time) | На 30–40% быстрее | Проще (автогенерация) |
Как мы разрабатываем плагин: этапы
- Анализ требований: какие нативные функции нужны, какие разрешения, lifecycle. Оцениваем объём: 1–2 метода под одну платформу или 10+ с EventChannel для обеих.
- Проектирование Dart API: контракт через Pigeon или ручные MethodChannel/EventChannel. Определяем типы данных.
- Реализация на iOS (Swift) и Android (Kotlin): используем
FlutterPlugin,ActivityAware,MethodCallHandler. Обработка всех edge cases и ошибок. - Тестирование: модульные тесты на Dart, интеграционные тесты на обеих платформах, тестирование на реальных устройствах.
- Интеграция в ваш проект: подключаем через git dependency или публикуем на pub.dev.
- Документация: README с API, примерами, CHANGELOG.
Типичные ошибки при работе с Platform Channels
- Двойной вызов
result.success()на Android — IllegalStateException: Reply already submitted. Гарантируем, что каждый вызов завершается ровно один раз. - Утечка
EventSinkпри повороте экрана на Android. Решение: обнуляемsinkвonCancel()и проверяем перед каждым вызовом. - Несовпадение имён методов между Dart и нативным кодом. Pigeon исключает эту проблему.
Структура плагина
Создаём через 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) } } } 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.
Что входит в работу
- Разработка Dart API и нативной реализации для iOS и Android
- Интеграция с вашим проектом (пример использования)
- Документация по API и сборке
- Публикация на pub.dev или предоставление доступа к git-репозиторию
- Гарантия стабильности платформенных каналов и обработка ошибок
Разработка плагина: простой (1–2 метода, одна платформа) — 2–4 дня. Полноценный cross-platform плагин с EventChannel, permissions и edge-case handling — 2–4 недели. Стоимость рассчитывается индивидуально. Получите консультацию: свяжитесь с нами для оценки вашего проекта. Мы уже реализовали более 30 кастомных плагинов для заказчиков из финтеха, медицины и IoT — присоединяйтесь. Закажите разработку плагина прямо сейчас.







