Разработка 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 в Dart. Явная проверка типов на нативной стороне обязательна.
Выделение в отдельный 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 — очистка handler. Без неё при горячей перезагрузке Flutter старый handler остаётся в памяти, и следующий вызов через канал срабатывает дважды.
Многопоточность: главная ловушка
Kotlin-часть MethodChannel.setMethodCallHandler вызывается на main thread. Любая блокирующая операция внутри — 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() должен вызываться на main thread — это требование 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: простой MethodChannel с 2-4 методами — 1-3 дня. EventChannel с управлением жизненным циклом и тестами — 3-5 дней. Оформление как переиспользуемого plugin — плюс 1-2 дня. Стоимость рассчитывается индивидуально.







