Створення Flutter-плагіна для нативної функціональності
Ви інтегруєте пропрієтарний SDK партнера — C++ бібліотеку з нативними викликами. Готових плагінів на pub.dev немає, а обгортка через ffi не підходить через складний lifecycle — наприклад, необхідно правильно керувати пам'яттю та синхронізацією з Dart-ізолятом. Знайома ситуація? Тоді пишемо власний плагін через Platform Channels. Ми, команда мобільних розробників з 10+ річним стажем, беремо такі задачі під ключ: від проектування API до публікації на pub.dev та інтеграції у ваш проект. У нас за плечима реалізація понад 40 кастомних плагінів для замовників з фінтеху, медицини та 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 тижні. Вартість розраховується індивідуально. Отримайте консультацію: зв'яжіться з нами для оцінки вашого проекту. Ми вже реалізували понад 40 кастомних плагінів для замовників з фінтеху, медицини та IoT — приєднуйтесь. Замовте розробку плагіна прямо зараз.







