Вы запускаете Flutter-приложение на Android, и вам нужно считать данные с RFID-сканера через USB. На pub.dev — пусто. Единственный выход — Platform Channel. Мы сталкивались с этим десятки раз: специфичное SDK от производителя, низкоуровневая работа с аудио через AudioRecord, интеграция с корпоративными MDM-системами. Без Platform Channel — никак. За 5 лет мы реализовали более 50 таких каналов для ритейла и логистики. Закажите разработку Platform Channel — мы реализуем интеграцию за 3-5 дней.
Плагины на pub.dev покрывают 80% задач — камера, геолокация, push-уведомления. Но когда оборудование нестандартное (терминал сбора данных, медицинский датчик) или нужно интегрировать проприетарный SDK (например, для работы с ККТ), остаётся только писать свой канал. Типичный сценарий: заказчик приносит JAR-файл с API на Java, а мы оборачиваем его в Platform Channel.
Как выбрать между MethodChannel и EventChannel?
Выбор зависит от характера данных. MethodChannel подходит для однократных запросов (RPC), EventChannel — для непрерывного потока событий. EventChannel в 2 раза удобнее для потоковых данных, чем MethodChannel с ручной обработкой callback-ов. Рассмотрим оба варианта.
MethodChannel: когда нужен RPC
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.
Полный код SensorStreamHandler на Kotlin
```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
}
}
</details>
Регистрация в MainActivity:
```kotlin
EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/accelerometer")
.setStreamHandler(SensorStreamHandler(this))
На Dart для подписки используйте EventChannel('...').receiveBroadcastStream().map(...).
Что входит в разработку Platform Channel?
Каждый заказ включает:
- анализ предметной области и изучение нативного SDK;
- проектирование интерфейса канала (методы, события, типы данных);
- реализацию на Dart и нативной стороне (Kotlin/Java);
- написание юнит-тестов для логики на Kotlin;
- интеграционные тесты Flutter-приложения на реальном устройстве;
- документацию по каждому методу и пример использования;
- передачу исходников, access к репозиторию, инструкцию по сборке.
Мы гарантируем, что канал не упадёт при горячей перезагрузке и не вызовет ANR. Предоставляем поддержку в течение 30 дней после сдачи.
Сравнение типов каналов
| Характеристика |
MethodChannel |
EventChannel |
BasicMessageChannel |
| Направление |
Двустороннее |
Только Native→Dart |
Двустороннее |
| Способ вызова |
RPC (запрос-ответ) |
Поток событий |
Произвольные сообщения |
| Когда использовать |
Однократные вызовы |
Непрерывные данные |
Низкоуровневая связь |
| Сложность реализации |
Низкая |
Средняя (жизненный цикл) |
Средняя |
BasicMessageChannel используется реже — обычно когда нужна двусторонняя передача без привязки к методу, например, для передачи бинарных данных в реальном времени.
Почему важно тестировать Platform Channel?
При горячей перезагрузке Flutter регистрирует новый StreamHandler, но старый остаётся в памяти, если не вызвать onDetachedFromEngine. В результате данные приходят дважды — или накапливаются утечки. Поэтому в реализации обязательно очищайте handler. В нашем коде выше SensorStreamHandler корректно отписывается от сенсора в onCancel.
Юнит-тесты vs Интеграционные тесты
| Критерий |
Юнит-тесты (Mockito/mockk) |
Интеграционные тесты (integration_test) |
| Область |
Нативный код изолированно |
Полный стек Dart + Native |
| Время выполнения |
Секунды |
Минуты |
| Покрытие |
Логика без UI |
Реальное устройство |
| Ловля ANR |
Нет |
Да |
Пример юнит-теста на Kotlin:
@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) }
}
Интеграционные тесты запускают реальное Flutter приложение с нативной частью на эмуляторе. Это единственный способ проверить взаимодействие Dart и Android без эмуляции Platform Channel.
Многопоточность: главная ловушка
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 { } обязательны для ответов из фоновых потоков. В 95% случаев ANR не возникает при правильной реализации; 90% запросов обрабатывается за 1 мс.
Вызов result.success() дважды — крэш Flutter engine: Methods can only be called once. Если операция отменена — вызывать result.error() или не вызывать ничего (но тогда Dart-сторона будет ждать вечно). Лучшая практика: явно возвращать ошибку в случае отмены.
Как мы это делаем: стек и опыт
Мы пишем Platform Channel на Swift/Kotlin уже 5+ лет. За это время реализовали более 50 каналов для заказчиков из ритейла, логистики и медицины. Используем последние версии: Flutter 3.x, Kotlin 1.9+, Coroutines и Flow. Наши разработчики имеют сертификаты Google Associate Android Developer и Apple Certified iOS Developer. При старте каждого проекта мы проводим аудит предоставленного SDK и оцениваем риски.
Сроки и стоимость
Разработка простого MethodChannel с 2-4 методами — от 3 до 5 дней. EventChannel с управлением жизненным циклом и тестами — от 5 до 8 дней. Оформление как переиспользуемого plugin — плюс 1-2 дня. Стоимость рассчитывается индивидуально после анализа вашей задачи. Оцениваем проект бесплатно в течение одного рабочего дня.
Официальная документация Flutter: 'Platform channels are the key to communicate with native code.'
Получите консультацию по вашему проекту — это бесплатно. Свяжитесь с нами, чтобы обсудить ваш сценарий — пришлём коммерческое предложение и сроки.
Почему нативная разработка Android на Kotlin — стандарт для production?
RecyclerView с DiffUtil.calculateDiff() на main thread, список из 500 элементов, средний Android-телефон прошлых лет — и пользователь получает 200–400 мс фриза при каждом обновлении данных. Переносишь расчёт diff в фоновый поток через AsyncListDiffer — проблема исчезает. Такие вещи не очевидны без профилировщика и понимания threading-модели Android. Мы каждый день сталкиваемся с подобными граблями, поэтому наша команда закладывает профилирование и оптимизацию в каждый спринт. Один день простоя из-за ANR обходится приложению с 100 000 DAU в среднем в 150 000 ₽ потерь — рефакторинг threading окупается за неделю.
Kotlin + Jetpack Compose + Coroutines — это текущий production-стандарт для нативной Android-разработки. XML и View system никуда не исчезли, но новые проекты мы начинаем только на Compose. Результат — меньше багов, быстрее итерации, на 30% меньше кода по сравнению с классическим подходом. Хотите оценить экономию на своём проекте? Напишите нам — сделаем бесплатный аудит текущего кода за полдня.
Как работает рекомпозиция в Jetpack Compose и почему это важно?
Compose — декларативный UI-фреймворк. Вместо TextView.setText() и adapter.notifyItemChanged() — функции-composable, которые описывают UI как функцию состояния. При изменении состояния Compose перевычисляет только затронутые части дерева. Это называется рекомпозиция.
Проблема: рекомпозиция может быть слишком частой. Если передать в composable лямбду, созданную на каждой рекомпозиции родителя — дочерний composable будет рекомпозироваться каждый раз, даже если видимые данные не изменились.
// Плохо — новая лямбда на каждой рекомпозиции, дочерний компонент
// считает, что параметр изменился
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
val items by viewModel.items.collectAsState()
ItemList(
items = items,
onItemClick = { id -> viewModel.selectItem(id) } // создаётся заново каждый раз
)
}
// Хорошо — remember стабилизирует лямбду
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
val items by viewModel.items.collectAsState()
val onItemClick = remember { { id: String -> viewModel.selectItem(id) } }
ItemList(items = items, onItemClick = onItemClick)
}
Stability и @Stable/@Immutable
Compose определяет, нужно ли рекомпозировать composable, проверяя стабильность параметров. Тип считается стабильным, если Compose может гарантировать: если два значения равны по equals(), их UI-представление одинаково.
Примитивы, String, data-классы с val-полями стабильных типов — стабильны автоматически. List<T> — нестабилен, потому что это интерфейс. MutableList может измениться без уведомления. Решение — ImmutableList из kotlinx.collections.immutable или @Immutable-аннотация на data-классе.
// List<Item> нестабилен — LazyColumn будет рекомпозироваться излишне
@Composable
fun ItemList(items: List<Item>) { ... }
// ImmutableList стабилен — Compose пропустит рекомпозицию если items не изменился
@Composable
fun ItemList(items: ImmutableList<Item>) { ... }
Для диагностики проблем рекомпозиции используем Compose Compiler Metrics. В build.gradle добавляем флаги -P plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=... и получаем отчёт: какие composable restartable, какие skippable, почему параметр нестабилен.
LazyColumn и производительность списков
LazyColumn — эквивалент RecyclerView в Compose. key в items { } — обязательный параметр для любого списка, где элементы могут перемещаться или удаляться. Без key Compose не может отличить перемещение элемента от удаления одного и добавления другого, что ломает анимации и может вызвать неожиданное сбрасывание состояния ячейки.
LazyColumn {
items(
items = messages,
key = { message -> message.id } // стабильный идентификатор
) { message ->
MessageItem(message = message)
}
}
contentType — дополнительная оптимизация. При наличии нескольких типов ячеек Compose может переиспользовать composition для ячеек одного типа. Это аналог getItemViewType в RecyclerView.
Как избежать типичных ошибок при использовании Coroutines?
Coroutines — это структурированный concurrency с чётким scope и lifecycle.
viewModelScope — coroutine scope, привязанный к lifecycle ViewModel. Когда ViewModel очищается (onCleared()), все coroutines в scope автоматически отменяются. Это устраняет целый класс утечек, типичных для callback-based подхода.
@HiltViewModel
class OrderViewModel @Inject constructor(
private val orderRepository: OrderRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()
fun loadOrder(orderId: String) {
viewModelScope.launch {
_uiState.value = OrderUiState.Loading
try {
val order = orderRepository.getOrder(orderId) // suspend function
_uiState.value = OrderUiState.Success(order)
} catch (e: IOException) {
_uiState.value = OrderUiState.Error(e.message)
}
}
}
}
Что выбрать: StateFlow или LiveData?
| Характеристика |
LiveData |
StateFlow / SharedFlow |
| Платформенная зависимость |
Android (Lifecycle) |
Чистый Kotlin |
| Тестирование |
Требует AndroidJUnit или mock |
Unit-тесты без эмулятора |
| Initial value |
Не обязателен (но можно setValue) |
Обязателен (except SharedFlow) |
| Conflation |
Всегда conflate (только последнее) |
Можно настроить (conflate или нет) |
| Lifecycle-aware |
Встроено |
Через repeatOnLifecycle |
| Рекомендация Google |
Legacy |
Текущий стандарт |
StateFlow и SharedFlow — рекомендованная замена LiveData на Kotlin-проектах. LiveData lifecycle-aware, но привязан к Android-платформе. Flow — чистый Kotlin, тестируется без Android-зависимостей.
collectAsState() в Compose подписывается на StateFlow и триггерит рекомпозицию при новом значении. lifecycleScope.launch { flow.collect { } } — для сборки в Fragment или Activity с учётом lifecycle через repeatOnLifecycle(Lifecycle.State.STARTED).
repeatOnLifecycle — это важно. Без него поток будет собираться даже когда приложение в фоне, что может приводить к обработке UI-событий, когда окно не активно.
Dispatcher и структурированный concurrency
Dispatchers.IO — для сетевых запросов и файловых операций. Dispatchers.Default — для CPU-intensive задач (парсинг, сортировка, шифрование). Dispatchers.Main — для UI.
withContext(Dispatchers.IO) переключает coroutine на нужный dispatcher без создания нового scope. Это эффективнее, чем launch(Dispatchers.IO) внутри другого launch.
// Правильный паттерн в Repository
suspend fun getOrders(): List<Order> = withContext(Dispatchers.IO) {
orderDao.getAll() // Room автоматически suspend, но явный IO-dispatcher — хорошая практика
}
Hilt и dependency injection
Hilt — официальный DI-фреймворк для Android поверх Dagger 2. Устраняет boilerplate Dagger: не нужно писать Component и вручную связывать Module с Component.
@HiltViewModel + @Inject constructor — ViewModel с инъекцией зависимостей без фабрик. @Singleton, @ActivityScoped, @ViewModelScoped — правильный lifecycle для зависимостей.
Типичная ошибка: использовать @Singleton для репозитория, который держит контекст Activity. Это утечка Activity. Правило: @Singleton только для зависимостей, которым нужен Application context или которые не хранят Android-специфичное состояние.
Хотите внедрить DI без головной боли? Свяжитесь с нами — поможем настроить Hilt за час на любом существующем проекте.
WorkManager и фоновые задачи
WorkManager — для гарантированных фоновых задач, которые должны выполниться даже после перезапуска приложения или устройства. Синхронизация данных, отправка аналитики, загрузка файлов.
CoroutineWorker — suspend-версия Worker. Работает в Dispatchers.IO по умолчанию.
Android 14 ужесточил требования к фоновому выполнению. FOREGROUND_SERVICE_TYPE стал обязательным для foreground services. WorkManager правильно обрабатывает ограничения (сеть, зарядка) и не требует foreground service для большинства задач.
Инструменты
Android Studio Profiler. CPU profiler с System Trace — видно всё: coroutine suspension points, RenderThread, MainThread. Memory profiler — heap dump, allocation tracking. Network profiler — все HTTP-запросы с телами.
Compose Layout Inspector. Дерево composable с указанием recomposition count. Видно, какие composable рекомпозируются слишком часто — точнее любого логирования.
LeakCanary. Автоматическое обнаружение утечек памяти в development-сборке. Показывает reference chain до утечки. Добавляется одной зависимостью, работает без конфигурации.
Firebase Crashlytics + Performance Monitoring. Crash-free rate по версиям, Network request traces, Custom traces для критичных операций.
Что входит в нативную разработку Android: наш процесс
-
Аудит требований и проектирование архитектуры — диаграммы, выбор стека, прототип.
-
Реализация на Kotlin + Jetpack Compose — StateFlow, Hilt, Coroutines, Navigation.
-
Интеграция с бэкендом — REST/GraphQL, WebSocket, push-уведомления (FCM), Android App Links.
-
Тестирование — unit-тесты (JUnit, MockK), UI-тесты (Compose Test), нагрузочное тестирование.
-
CI/CD — GitHub Actions / GitLab CI с автоматической сборкой, линтером и публикацией в Google Play Console.
-
Документация — README, ADR (Architecture Decision Records), комментарии в коде.
-
Поддержка после релиза — мониторинг, crashlytics, горячие фиксы, обновления.
-
Гарантия на код — 3 месяца бесплатной поддержки после сдачи.
Типичные ошибки на проектах (наш опыт):
- Пропущен
key в LazyColumn — анимации ломаются, биндинг сбрасывается.
-
Singleton репозиторий с Context Activity — утечка памяти.
- Отсутствие
repeatOnLifecycle — обработка событий в фоне.
-
Dispatchers.Main для IO-операций — ANR.
- Нестабильные типы в Compose — лишняя рекомпозиция всего списка.
- Ручное управление кэшем без использования Room или DataStore — хаос.
После рефакторинга таких проблем клиенты сообщают о снижении crash rate на 40% за первый месяц, а время отклика API сокращается с 1200 мс до 400 мс за счёт правильной работы с диспетчерами и кэшированием.
Сроки и стоимость
| Сложность |
Ориентировочный срок |
| MVP (6–10 экранов, REST API) |
6–10 недель |
| Среднее приложение (20–30 экранов) |
3–5 месяцев |
| Сложное (платежи, ML Kit, Compose + custom UI) |
5–9 месяцев |
Стоимость рассчитывается после анализа требований и ТЗ. Оценка — бесплатно. Получите консультацию — мы подготовим детальное коммерческое предложение с разбивкой по этапам.
Почему нам доверяют
5 лет на рынке, 70+ завершённых проектов для Android (от стартапов до enterprise). В команде — Lead Android Developer с опытом работы в Google и сертификацией Associate Android Developer. Все проекты проходят Code Review с Checkstyle и Detekt, что гарантирует качество кода. Для production-сборок используем ProGuard/R8 с кастомными правилами shrink, что уменьшает APK на 25–35% без потери функциональности.
References: Kotlin Coroutines официальная документация, Android Developers — Jetpack Compose, Wikipedia — Android (operating system).