Нативные SDK через Method Channels
Flutter собирает из одной кодовой базы приложения для мобильных, веба, десктопа и встраиваемых устройств: логика пишется на Dart один раз и компилируется под каждую платформу нативно.
Разработчику, привыкшему к Kotlin, Java, Swift или Objective-C, Flutter оставляет полный доступ к платформенным возможностям. Мост между Dart и нативным кодом называется MethodChannel.
Это дверь в обе стороны: Dart может попросить Android или iOS выполнить код, а платформа — вернуть результат. Обмен идёт сообщениями, а не через общую память, поэтому схема проста и безопасна.
Через MethodChannel можно:
- вызывать API Android и iOS, которых нет во Flutter;
- использовать сторонние SDK — сканеры штрихкодов, работу с Bluetooth, нативные элементы интерфейса;
- выполнять операции, требующие нативной скорости или доступа к устройству;
- быстро возвращать данные из нативного кода во Flutter.
Что это даёт разработчику с нативным опытом
| Задача | Как помогает MethodChannel |
|---|---|
| Дотянуться до любого API платформы | Любой вызов Android или iOS доступен из Flutter привычным нативным кодом. |
| Не тормозить интерфейс | Сообщения кодируются в двоичный вид и обрабатываются асинхронно. |
| Оставить критичный код нативным | Чувствительная к скорости и безопасности логика остаётся на Kotlin или Swift, интерфейс собирается на Dart. |
| Подключить SDK без обёртки | Нативную библиотеку без поддержки Flutter можно использовать, не дожидаясь плагина. |
Годы платформенного опыта при переходе на Flutter не пропадают: MethodChannel позволяет применить их ровно там, где нужно.
Что такое MethodChannel
MethodChannel, он же платформенный канал, — основной механизм Flutter для работы с возможностями платформы. Код на Dart отправляет сообщения нативному коду — Kotlin или Java на Android, Swift или Objective-C на iOS — и получает ответы. Так приложение добирается до функций устройства и сторонних библиотек, которых нет ни во Flutter, ни в его плагинах.
Пример:
class _BatteryLevelScreenState extends State<BatteryLevelScreen> {
// Имя канала должно в точности совпадать с именем на нативной стороне.
static const platform = MethodChannel('com.example.battery');
// Здесь будет храниться уровень заряда.
String _batteryLevel = 'Unknown battery level.';
// Вызываем нативный метод.
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
// Обращение к нативной стороне.
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result%.';
} on PlatformException catch (e) {
// Обработка ошибки нативного кода.
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
// Обновляем интерфейс.
setState(() {
_batteryLevel = batteryLevel;
});
}
Канал работает по модели передачи сообщений. Вы задаёте уникальное имя канала — например, 'com.example/device' — и используете его с обеих сторон. Dart вызывает метод через invokeMethod(), передавая при необходимости данные. Нативная сторона регистрирует обработчик, который ждёт вызовов, выполняет код и возвращает результат.
Обмен асинхронный и несвязанный:
- Dart не ждёт нативный код: возвращается
Future, который завершается по готовности результата; - нативная сторона обязана явно вернуть
success,errorилиnotImplemented.
Ключевые понятия
- Имя канала — строка, одинаковая с обеих сторон, например
'com.example/platform'. Чтобы имена не пересекались, их строят от домена приложения или организации. - Вызов метода —
invokeMethod('methodName', arguments). Имя метода — обычная строка, аргументы —nullили любое значение, поддерживаемоеStandardMessageCodec: bool, int, double, string, List, Map. - Обработчик — нативный код регистрирует его (
setMethodCallHandlerна Android) и выполняет логику при совпадении имени метода. - Ответ — обработчик обязан вернуть
result.success(...),result.error(...)илиresult.notImplemented(). Этот ответ и завершаетFutureна стороне Dart.
Как идёт сообщение

Такая схема разделяет платформенную логику и интерфейс, не блокирует поток интерфейса ни с одной стороны и легко расширяется: методов на одном канале может быть сколько угодно, а каналов — несколько.
Когда это нужно
- Требуется API Android или iOS, которого нет во Flutter и плагинах, — например, доступ к конкретному датчику или нативному хранилищу.
- Нужно подключить закрытый или фирменный SDK: аналитику, платежи, распознавание текста.
- Нужно открыть нативный экран: полноэкранный сканер, системный выбор файла.
- Вы переносите на Flutter существующее нативное приложение по частям.
Чем MethodChannel не является
- Это не общая память. Все данные копируются через сериализацию. Поддерживаются только стандартные типы — примитивы, списки, словари, типизированные массивы. Передача больших объёмов требует полной сериализации и разбора.
- Это не синхронный вызов. Вызовы сразу возвращают
Futureи ничего не блокируют; результат приходит через цикл событий, а ошибки платформы — исключениемPlatformException. - Это не готовый контракт. Имена методов, аргументы и типы вы задаёте сами с обеих сторон. Проверки на этапе компиляции нет — расхождение всплывёт только при работе приложения. Поэтому контракт стоит описать и проверять входные данные.
Где это применяется
Плагины Flutter покрывают многое, но не всё. Когда нужного плагина нет или он не подходит, MethodChannel даёт прямой путь.
По сути, такая интеграция — это и есть разработка плагина: вы пишете тот же нативный мост, только упакованный под своё приложение, а не как публичный пакет. Готовый результат подключается к FlutterFlow.
Возможности устройства, которых нет в плагинах
Пример: уровень сигнала сотовой сети, подробная информация о батарее, тепловое состояние устройства.
- Низкоуровневые API вроде
TelephonyManagerна Android илиCoreTelephonyна iOS через плагины почти не доступны. - Им нужны свои разрешения и прямой нативный вызов.
- Через MethodChannel вы берёте ровно то, что требуется, не дожидаясь обновления плагина и не написав его целиком.
Что это даёт: доступ к телеметрии и диагностике — тому, на чём держатся приложения для выездных работ, тестовые инструменты и корпоративная отчётность.
Закрытые SDK поставщиков
Пример: SDK проверки личности, сканер документов, защищённое хранилище.
- Многие поставщики выпускают только SDK для Android и iOS, без обёрток для Flutter.
- Небольшая нативная обёртка и канал открывают ровно ту часть, которая нужна.
- Обновления SDK не задевают интерфейс на Flutter.
Что это даёт: ключевые функции — проверка клиента, биометрия, платежи — работают без зависимости от авторов плагинов.
Временный показ нативного экрана
Пример: нативный просмотрщик PDF, камера из SDK поставщика, интерфейс дополненной реальности.
PlatformViewумеет встраивать нативные представления, но требует больше настройки и хуже сказывается на производительности.- Если нативный экран открывается временно и на весь экран, проще вызвать его через MethodChannel и вернуть управление Flutter.
Что это даёт: нативный опыт там, где он нужен, без вмешательства в остальную отрисовку.
Фоновые задачи и события платформы
Пример: вход в геозону, обновление токена уведомлений, изменение состояния Bluetooth-устройства.
- Эти события возникают в нативных службах и фоновых задачах.
- События можно накапливать на нативной стороне и передавать во Flutter, когда приложение активно.
- Для непрерывного потока данных берут
EventChannel— MethodChannel рассчитан на разовые обмены.
Что это даёт: интеграцию на уровне системы — местоположение, питание, Bluetooth — без опроса и сложной логики на Dart.
Защищённые данные устройства
Пример: IMEI, MAC-адрес, отпечаток устройства, системные идентификаторы.
- Такие API часто требуют особых прав и нативных запросов разрешений.
- Нативный код проверяет права, фильтрует данные и решает, что безопасно возвращать.
Что это даёт: работа с чувствительными данными остаётся под нативным контролем — то, что требуется в корпоративной и регулируемой среде.
Реализация
Разберём канал целиком: объявление на Dart, подключение нативного кода и обмен аргументами и результатами.
1. Сторона Dart
Канал создаётся классом MethodChannel из пакета services. Вызов всегда начинает Dart, нативная сторона отвечает.
Объявление канала:
import 'package:flutter/services.dart';
const platform = MethodChannel('com.example/device');
- Имя
'com.example/device'должно в точности совпадать с нативной стороной. - Имена строятся от домена в обратном порядке — так они не пересекаются.
Вызов метода:
Future<String> getBatteryLevel() async {
try {
final int result = await platform.invokeMethod('getBatteryLevel');
return 'Battery level: $result%';
} on PlatformException catch (e) {
return 'Failed to get battery level: ${e.message}';
}
}
invokeMethodпередаёт имя метода и, при необходимости, аргументы.- Результат приходит асинхронно через
Future. - Вызов всегда оборачивают в
try-catch:PlatformExceptionвозникает, если нативный код выбросил ошибку, метод не реализован или не удалась сериализация данных.
Что учесть:
- Аргументы передаются вторым параметром
invokeMethod()— например,Map<String, dynamic>. - Результатом может быть любой тип, совместимый с JSON:
int,String,bool,double,List,Map.
2. Сторона Android (Kotlin)
Обработчик регистрируется в MainActivity и по умолчанию выполняется в главном потоке, поэтому долгие операции нужно выносить в фоновый.
Импорты:
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.os.BatteryManager
import android.content.Context
Обработчик:
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example/device"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
when (call.method) {
"getBatteryLevel" -> {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
}
else -> result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}
Что учесть:
- Ответ возвращается ровно одним из трёх способов:
result.success(data)— данные уходят в Dart;result.error(code, message, details)— в Dart возникнетPlatformException;result.notImplemented()— в Dart возникнетMissingPluginException.
- Вызывать
resultдважды нельзя: на один вызов метода Flutter ждёт ровно один ответ. - Если внутри есть ввод-вывод, сеть или что-то блокирующее, выносите это в фоновый поток:
Thread(Runnable {
val resultData = longRunningOperation()
runOnUiThread {
result.success(resultData)
}
}).start()
3. Сторона iOS (Swift)
Канал обрабатывается через FlutterMethodChannel в AppDelegate.swift; обработчик так же по умолчанию выполняется в главном потоке.
Объявление канала:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "com.example/device",
binaryMessenger: controller.binaryMessenger)
Обработчик:
batteryChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
if call.method == "getBatteryLevel" {
UIDevice.current.isBatteryMonitoringEnabled = true
let level = UIDevice.current.batteryLevel
if level >= 0 {
result(Int(level * 100))
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "Battery level not available.",
details: nil))
}
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Что учесть:
- На каждый вызов — ровно один ответ.
- Подробности об ошибке передаются через
FlutterError. - Долгие операции выполняйте в
DispatchQueue.global().async, а результат возвращайте черезDispatchQueue.main.async.
Что стоит делать
- Держать имена каналов и методов одинаковыми с обеих сторон.
- Обмениваться только стандартными типами:
String,int,bool,List,Map. - Явно обрабатывать ошибки на обеих сторонах.
- Выносить долгие операции в фоновые потоки.
- Держать нативную часть тонкой и тестируемой, отделяя логику SDK от кода канала.
MethodChannel во FlutterFlow
FlutterFlow генерирует полноценное приложение на Flutter и позволяет писать свой код на Dart через собственные действия и функции. А вот править нативный код — Kotlin и Swift — прямо в веб-интерфейсе нельзя. Отсюда и порядок работы: нативная часть выносится в отдельный плагин.
Шаг 1. Собрать свой плагин
1.1 Создать заготовку
flutter create --template=plugin --platforms=android,ios my_custom_plugin
Команда создаёт структуру плагина для Android и iOS.
1.2 Написать нативный код
В папках android и ios реализуйте нужную функциональность. Для примера с уровнем заряда:
- Android (Kotlin): в
MyCustomPlugin.ktобратитесь кBatteryManager. - iOS (Swift): в
MyCustomPlugin.swiftиспользуйтеUIDevice.
1.3 Опубликовать на GitHub
- Создайте репозиторий Git в папке плагина.
- Зафиксируйте изменения.
- Отправьте репозиторий на GitHub.
Проверьте pubspec.yaml и расставьте теги версий.
Шаг 2. Подключить плагин к проекту FlutterFlow
-
Откройте Custom Code > Custom Actions или Custom Widgets.
-
Создайте собственное действие или виджет.
-
В панели Settings справа найдите Dependencies.
-
Добавьте плагин по адресу репозитория:
my_custom_plugin: git: url: https://github.com/yourusername/my_custom_plugin.git -
В редакторе кода добавьте импорт:
import 'package:my_custom_plugin/my_custom_plugin.dart'; -
Используйте API плагина.
Подробнее — в разделе про подключение неопубликованных и закрытых пакетов.
Шаг 3. Вызвать плагин из собственного действия
-
Создайте собственное действие.
-
Вызовите в нём плагин:
Future<int> getBatteryLevel() async { final batteryLevel = await MyCustomPlugin.getBatteryLevel(); return batteryLevel; } -
Скомпилируйте код и убедитесь, что ошибок нет.
-
Используйте действие в потоках наравне со встроенными.
Закрытые репозитории
Если репозиторий плагина закрытый, FlutterFlow нужен доступ — учётные данные или ключ SSH. Как это настроить, описано в разделе про неопубликованные и закрытые пакеты.
Типичные ошибки и отладка
Проблемы с каналами почти всегда сводятся к расхождению сторон или к недоделанной обработке ошибок. Разберём то, что встречается чаще всего.
MissingPluginException
Симптом: Flutter выбрасывает MissingPluginException — метод или плагин не реализован.
Что произошло: Dart вызвал метод, но нативная сторона не узнала ни канал, ни имя метода.
Причины:
- имя канала в Dart не совпадает с нативным;
- обработчик
setMethodCallHandlerне зарегистрирован или зарегистрирован не там; - нативный код был перезаписан при повторной выгрузке проекта из FlutterFlow;
- метод вызван до того, как движок Flutter и канал полностью инициализировались;
- сработала горячая перезагрузка — она обновляет только код Dart.
Что делать:
- сверить имена каналов посимвольно, с учётом регистра;
- на Android регистрировать канал внутри
configureFlutterEngine(); - на iOS — внутри
didFinishLaunchingWithOptions(); - залогировать регистрацию каналов при запуске приложения;
- после правки нативного кода делать полный перезапуск: платформенная часть требует пересборки.
Несовпадение типов
Симптом: приложение падает на приведении типов или неожиданно возвращает null.
Что произошло: данные, которыми обменялись стороны, оказались не того вида.
Причины:
- Dart отправляет Map, а нативная сторона ждёт String, или наоборот;
- нативный код возвращает объект платформы, который Flutter не может сериализовать;
- тип результата не поддерживается
StandardMessageCodec.
Что делать:
- пользоваться только стандартными типами:
int,double,String,bool,List,Mapс безопасным для JSON содержимым; - на стороне Dart указывать ожидаемый тип:
invokeMethod<int>(...); - на нативной стороне проверять типы до использования, применять безопасное приведение;
- не отправлять сложные объекты вроде ответов нативного SDK — превращать их в простой словарь или строку.
Нет ответа или зависание
Симптом: вызов invokeMethod() не возвращается или интерфейс замирает.
Что произошло: нативная сторона не завершила вызов либо заняла главный поток долгой операцией.
Причины:
- обработчик не вызвал ни
result.success, ниresult.error, ниresult.notImplemented; - внутри обработчика возникло исключение, и ответ не ушёл;
- тяжёлая работа — файлы, сеть — выполняется в главном потоке.
Что делать:
- вызывать ровно один из трёх ответов, всегда;
- оборачивать нативный код в try/catch и сообщать об исключениях;
- выносить долгие операции в фоновый поток, корутину или очередь;
- показывать индикатор загрузки и предусматривать таймаут на стороне Dart.
Повторный вызов result
Симптом: ошибка вида «Reply already submitted» или непредсказуемые результаты.
Что произошло: нативный код ответил на один вызов больше одного раза.
Причины:
- из-за ошибки в логике выполнились обе ветки — и успешная, и с ошибкой;
- асинхронные операции наперегонки возвращают ответ;
- таймаут, повтор или исключение вызывают второй ответ.
Что делать:
- отслеживать факт ответа флагом, например
var responded = false; - прерывать выполнение
returnпосле ответа; - выстраивать асинхронные вызовы так, чтобы отрабатывала ровно одна ветка.
Отладка по платформам
Flutter и Dart:
- логируйте вызовы и результаты через
print()илиdebugPrint(); - оборачивайте
invokeMethodв try/catch и пишите исключения в журнал; - ставьте записи до и после вызова, чтобы видеть ход выполнения;
- смотрите журнал и состояние приложения в Flutter DevTools.
Android (Kotlin, Java):
Log.d("MethodChannel", "Received: ${call.method}")внутри обработчика;- фильтрация журнала:
adb logcat | grep flutter; - проверьте, что
configureFlutterEngine()действительно вызывается — в старых проектах это могло требовать ручной настройки; - пользуйтесь точками останова в Android Studio.
iOS (Swift, Objective-C):
- трассируйте обработчик через
print()илиNSLog(); - следите за журналом Xcode при запуске — там видны проблемы регистрации канала;
- проверьте, что
result(...)вызывается ровно один раз; - убедитесь, что
AppDelegateкорректно приводитwindow?.rootViewControllerкFlutterViewController.
Производительность и архитектура
FlutterFlow генерирует чистый код на Dart и позволяет вставлять свою логику через собственные действия, но править нативный код — Kotlin и Swift — внутри платформы нельзя.
Не занимайте главный поток
- Все вызовы MethodChannel по умолчанию обрабатываются в главном потоке, который отвечает и за отрисовку.
- Работа с базой, файлами, Bluetooth и сетью обязана уходить в фоновый поток.
- На Android это
Executorsили корутины, на iOS —DispatchQueue.global(). - Результат возвращайте в главный поток:
runOnUiThreadна Android,DispatchQueue.main.asyncна iOS.
Даже несколько миллисекунд блокировки дают пропущенные кадры и рваную анимацию — особенно заметно на недорогих устройствах.
Внутри FlutterFlow всё выглядит гладко, но на реальном устройстве медленная операция в Kotlin или Swift заморозит приложение. Выносите такие задачи в фон до возврата в Dart.
Держите код канала тонким
- Обработчик канала — это контроллер, а не сервис: он должен передавать работу нормальным нативным компонентам.
- Тонкий слой между Dart и нативным кодом проще сопровождать.
- Например,
getBatteryLevelв Kotlin должен просто вызватьBatteryService().getLevel(). - Тогда нативная часть развивается независимо от интерфейса.
Такое разделение даёт лучшее покрытие тестами и избавляет от ошибок, которые трудно отследить между слоями.
Только данные, совместимые с JSON
- Обмен идёт через
StandardMessageCodec. - Он поддерживает ограниченный набор типов:
int,double,bool,String,List,Mapиnull. - Нативные типы —
Bitmap,Bundle,NSData,UIColor— нужно сначала превратить в структуру, дружественную JSON. - Сложные данные, например результат сканирования штрихкода или сведения об устройстве, сериализуйте в плоский словарь или строку JSON.
Несовпадение типов не поймается при компиляции — оно уронит приложение во время работы.
Если собственное действие вызывает MethodChannel, убедитесь, что возвращаемое значение можно использовать во FlutterFlow. В состоянии приложения, условиях и виджетах работают только поддерживаемые типы вроде String и int.
Проверяйте входные данные
- Относитесь к вызовам из Dart как к запросам извне: они могут прийти в любом виде.
- Разбирайте имена методов через
whenилиswitch. - Проверяйте наличие и тип аргументов до использования:
val timeout = call.argument<Int>("timeout") ?: return result.error("INVALID", "Missing timeout", null)
- Защитный код избавляет от неожиданного поведения, нативных падений и неверной работы с оборудованием.
Собственные действия во FlutterFlow принимают параметры из интерфейса, но если параметр не задан или передан неверно, код на Dart всё равно выполнится. Проверяйте такие значения на нативной стороне.
Логируйте с обеих сторон
- Пишите в журнал каждый вызов канала — и на Dart, и на нативной стороне:
- какой метод вызван;
- с какими аргументами;
- что вернулось и сколько это заняло.
- Пользуйтесь структурированными записями:
Log.d("MethodChannel", "method=... args=... result=...")на Android,NSLogилиprint()на iOS. - Согласуйте отметки времени между слоями — это сильно помогает при разборе.
Когда что-то ломается в продакшене, разница между десятиминутным исправлением и многодневным расследованием обычно упирается именно в журналы.
debugPrint() внутри собственных действий выводит записи рядом с платформенными. В тестовых сборках по ним видно, приходят ли нативные результаты.
Итог
MethodChannel — основной способ выйти за пределы того, что дают плагины: прямой доступ к нативным API и SDK, полный контроль над интеграцией.
Как и любая граница между системами, канал требует дисциплины. Небрежная реализация даёт хрупкий мост, просадки производительности и растущую стоимость поддержки. Аккуратная — наоборот:
- понятный интерфейс между Dart и нативным слоем;
- переиспользование оптимизированных под платформу SDK;
- возможность выпускать сложные функции, не дожидаясь плагинов;
- схему интеграции, которая выдерживает рост команды и продукта.
В будущем взаимодействие с платформой станет проще за счёт двух инструментов: FFIgen генерирует привязки к API Objective-C и Swift, JNIgen — к API Java и Kotlin.