Розробка Platform Channel для Flutter-додатків (Android)
Platform Channel — це міст між кодом Dart у Flutter та нативним Android-кодом (Kotlin/Java). Він потрібен, коли pub.dev не пропонує готового плагіна: SDK від виробника обладнання, низькорівневі операції з аудіо через AudioRecord або інтеграція з корпоративними MDM-системами. Існують три типи каналів: MethodChannel (RPC), EventChannel (потік подій з native у Dart), BasicMessageChannel (двосторонні довільні повідомлення).
MethodChannel: Виклик нативних методів з Dart
Сторона Dart:
class NfcService {
static const _channel = MethodChannel('com.example.app/nfc');
Future<bool> isNfcAvailable() async {
try {
return await _channel.invokeMethod<bool>('isNfcAvailable') ?? false;
} on PlatformException catch (e) {
debugPrint('NFC error: ${e.code} — ${e.message}');
return false;
}
}
Future<String?> readNfcTag() async {
return _channel.invokeMethod<String>('readNfcTag');
}
}
Сторона Kotlin (MainActivity.kt або окремий Handler):
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.app/nfc"
private lateinit var nfcAdapter: NfcAdapter
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"isNfcAvailable" -> result.success(nfcAdapter.isEnabled)
"readNfcTag" -> startNfcRead(result)
else -> result.notImplemented()
}
}
}
private fun startNfcRead(result: MethodChannel.Result) {
// впровадження читання NFC
}
}
Назва каналу — це рядок. Конвенція: com.назва_компанії.додаток/модуль. Невідповідність назв між Dart та Kotlin призводить до MissingPluginException під час виконання без будь-яких підказок під час збірки.
EventChannel: Потік подій у Dart
Для даних, які нативна сторона генерує постійно: показання датчиків, сповіщення Bluetooth GATT, зміни GPS.
Kotlin:
class SensorStreamHandler(private val context: Context) : EventChannel.StreamHandler {
private var sensorManager: SensorManager? = null
private var eventSink: EventChannel.EventSink? = null
private val sensorListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
eventSink?.success(mapOf(
"x" to event.values[0].toDouble(),
"y" to event.values[1].toDouble(),
"z" to event.values[2].toDouble(),
"timestamp" to event.timestamp
))
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
}
override fun onListen(arguments: Any?, sink: EventChannel.EventSink) {
eventSink = sink
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val accelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
if (accelerometer == null) {
sink.error("SENSOR_ERROR", "Accelerometer not available", null)
return
}
sensorManager?.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_UI)
}
override fun onCancel(arguments: Any?) {
sensorManager?.unregisterListener(sensorListener)
eventSink = null
sensorManager = null
}
}
Реєстрація у MainActivity:
EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/accelerometer")
.setStreamHandler(SensorStreamHandler(this))
Dart:
class AccelerometerService {
static const _channel = EventChannel('com.example.app/accelerometer');
Stream<Map<String, dynamic>> get accelerometerStream {
return _channel.receiveBroadcastStream().map(
(event) => Map<String, dynamic>.from(event as Map),
);
}
}
// Використання:
AccelerometerService().accelerometerStream.listen((data) {
setState(() {
x = data['x'] as double;
y = data['y'] as double;
});
});
Типи даних: що проходить через канал
Platform Channel автоматично конвертує між Dart та Kotlin наступні типи:
| Dart | Kotlin |
|---|---|
null |
null |
bool |
Boolean |
int |
Int / Long |
double |
Double |
String |
String |
Uint8List |
ByteArray |
List |
List<Any?> |
Map |
HashMap<Any, Any?> |
Важливо: int у Dart конвертується на Int, якщо відповідає, інакше Long. Якщо нативна сторона повертає Int, а Dart очікує int, проблеми немає. Але якщо Kotlin повертає Double, а Dart очікує int, ви отримуєте TypeError. Явна перевірка типів на нативній стороні є обов'язковою.
Виділення у окремий Plugin
Щоб переповторно використовувати Platform Channel у кількох проектах, упакуйте його як Flutter Plugin:
flutter create --template=plugin --platforms=android my_nfc_plugin
Точка входу — це клас, що реалізує FlutterPlugin:
class MyNfcPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(binding.binaryMessenger, "my_nfc_plugin")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
// обробник
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
onDetachedFromEngine очищує обробник. Без цього під час гарячої перезавантаження Flutter старий обробник залишається в пам'яті, й наступний виклик через канал срабативає двічі.
Багатопоточність: основна пастка
Kotlin-частина MethodChannel.setMethodCallHandler викликається на головному потоці. Будь-яка блокуюча операція всередину спричиняє ANR. Паттерн:
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"heavyOperation" -> {
CoroutineScope(Dispatchers.IO).launch {
val data = performHeavyOperation()
withContext(Dispatchers.Main) {
result.success(data)
}
}
}
}
}
result.success() має бути викликано на головному потоці—це вимога Flutter. withContext(Dispatchers.Main) або Handler(Looper.getMainLooper()).post { } обов'язкові для відповідей з фонових потоків.
Виклик result.success() двічі призводить до краху Flutter engine: Methods can only be called once. Якщо операція скасована, викличте result.error() або не викликайте нічого (але тоді сторона Dart чекає вічно). Найкраща практика: явно повертайте помилку при скасуванні.
Тестування
Юніт-тестування логіки Kotlin без Flutter:
@Test
fun `isNfcAvailable returns false when adapter disabled`() {
val mockAdapter = mockk<NfcAdapter> { every { isEnabled } returns false }
val handler = NfcMethodHandler(mockAdapter)
val result = mockk<MethodChannel.Result>(relaxed = true)
handler.handleIsNfcAvailable(result)
verify { result.success(false) }
}
Інтеграційні тести через пакет integration_test Flutter запускають реальний Flutter-додаток з нативною частиною на емуляторі.
Розробка Platform Channel займає 1–3 дні для простого MethodChannel з 2–4 методами. EventChannel з управлінням життєвим циклом та тестами займає 3–5 днів. Упакування як переповторно використовуваного плагіна додає 1–2 дні. Вартість розраховується індивідуально.







