Нативные плагины: когда гибрид упирается в потолок
Нативные плагины Capacitor на Swift и Kotlin: как они устроены, восемь примеров из практики, сколько стоит поддержка и когда пора уходить в чистый натив.
Потолок гибридного приложения наступает не там, где «не хватает производительности», а там, где нужен системный API, которого в вебе просто нет. Браузер не умеет зарегистрировать обработчик «Поделиться», не отдаст отпечаток пальца и не запустит фоновую обработку видео. В этот момент пишутся нативные плагины Capacitor: механизм для них встроен во фреймворк, класс на Swift и класс на Kotlin получают типизированный интерфейс в TypeScript, и на одну возможность обычно уходит 2-5 дней.
Архитектура от этого не разваливается. Приложение остаётся гибридным, 90% кода остаётся общим, а вниз, в платформу, спускается ровно та часть, которая иначе не решается. За два с половиной года владения приложением на iOS и Android я написал таких плагинов больше восьми.
Кроссплатформенная разработкаот 350 000 до 1 200 000 ₽сроки: 8-16 недель
Как устроен плагин
Три части, которые надо написать под каждую возможность.
Класс на Swift. Наследует CAPPlugin, методы помечаются @objc и принимают CAPPluginCall. Внутри - обычный нативный код: AVFoundation, LocalAuthentication, PhotosUI, что нужно. Результат отдаётся через call.resolve(), ошибка через call.reject().
@objc(MediaPlugin)
public class MediaPlugin: CAPPlugin {
@objc func compress(_ call: CAPPluginCall) {
guard let path = call.getString("path") else {
return call.reject("path is required")
}
// работа в фоновой очереди, resolve по завершении
}
}
Класс на Kotlin. Наследует Plugin, методы помечаются @PluginMethod и принимают PluginCall. Отдельная боль - жизненный цикл активити: если плагин открывает системный экран (камеру, выбор файла), результат приходит в колбэк, и вызов надо сохранить через bridge.saveCall(), иначе после поворота экрана или убийства процесса вы получите потерянный промис.
Интерфейс на TypeScript. Объявляется тип, регистрируется через registerPlugin, и дальше веб-слой работает с обычной асинхронной функцией:
export interface MediaPlugin {
compress(options: { path: string; bitrate?: number }): Promise<{ path: string; size: number }>;
}
export const Media = registerPlugin<MediaPlugin>('Media');
Всё, что ездит между слоями, сериализуется в JSON. Это накладывает главное ограничение проектирования: через мост нельзя гонять большие данные. Видео, фото и файлы передаются путями или идентификаторами, а не байтами. Если пытаться протащить base64 на 40 мегабайт, приложение упадёт по памяти - проверено.
Нативные плагины Capacitor, которые я писал
Share Extension на iOS и share-intent на Android. Самый сложный из всех. Задача: пользователь в любом чужом приложении жмёт «Поделиться» и отправляет фото или видео в наше. На iOS это отдельный таргет в Xcode с интерфейсом на SwiftUI, который живёт в собственном процессе и не имеет доступа к памяти основного приложения. Обмен идёт через App Group: расширение кладёт файлы в общий контейнер и пишет метаданные, основное приложение при следующем запуске их забирает. Плюс IPC, чтобы приложение узнало о новых данных, если оно уже запущено. На Android проще концептуально (intent-filter в манифесте), но всплывает своя история с временными URI и разрешениями на чтение чужого контента. Две недели работы, из них половина на пограничные случаи.
Обработка видео на FFmpeg. Обрезка, транскодирование, сжатие перед загрузкой. Пользователь снимает ролик на 200 мегабайт, а на сервер должно уехать 20. Всё считается на устройстве, чтобы не платить за трафик и серверные мощности. Отдельная возня с аппаратными кодеками: на разных Android-чипсетах поведение различается, и приходится держать программный запасной путь.
Поддержка HEIC. Айфоны снимают в HEIC, а половина мира его не читает. Плагин конвертирует в JPEG на устройстве с сохранением EXIF и ориентации. Мелочь, которая ломает загрузку фото у половины пользователей, если её не сделать.
Запись аудио и своя аудиоволна. Нативная запись с контролем формата и битрейта, отрисовка волны через wavesurfer в веб-слое. Гибридный случай в чистом виде: тяжёлое - в нативе, интерфейс - в вебе.
FaceID и TouchID. Вход и подтверждение операций через LocalAuthentication на iOS и BiometricPrompt на Android. Важная часть - не сама биометрия, а корректная работа с Keychain и Keystore: где лежит секрет, что происходит при добавлении нового отпечатка, как откатиться на пин-код.
Sign in with Apple. Обязателен, если в приложении есть вход через сторонние сервисы. Отдельная логика с тем, что почту Apple отдаёт один раз при первой авторизации, а дальше присылает только идентификатор. Если не сохранить её сразу, восстановить неоткуда.
Пуш-уведомления. Официальный плагин закрывает базу, но реальные требования всегда сложнее: своя обработка нажатия с переходом на конкретный экран, бейджи, тихие уведомления для фоновой синхронизации, разное поведение при открытом и закрытом приложении.
Клавиатура и safe area. Скучный, но обязательный плагин: он сообщает веб-слою высоту клавиатуры и системные отступы. Без него на iOS поле ввода уезжает под клавиатуру, а кнопка внизу оказывается под полосой жестов.
Разрез приложения: приложение в сторах
Куда встраивается нативный плагин
Веб-слой общий, мост общий, а ниже под каждую платформу пишется свой код на Swift и Kotlin.
iOS
App Store, ревью 1-3 дня
Android
Google Play, ревью до суток
Оболочка Capacitor
- WebView
- мост JS ↔ Native
- сборка .ipa и .aab
Нативные плагины
- камера и галерея
- push-уведомления
- геолокация
- биометрия
- файлы и шаринг
Один код Vue 3 + TypeScript
- экраны и навигация
- бизнес-логика
- состояние
- запросы к API
Нативного кода в проекте только оболочка и плагины. Экраны, логика и данные - общие, поэтому правка уезжает сразу на обе платформы.
Когда плагин писать не надо
Сначала ищем готовый. У Capacitor есть официальный набор: камера, файловая система, геолокация, уведомления, шаринг, браузер, статус сети, предпочтения, хаптика. Плюс большая экосистема сообщества и совместимость с частью старых плагинов Cordova - многие из них до сих пор работают, хотя выглядят как археология.
Порядок принятия решения у меня такой:
- Проверить, решается ли задача в вебе. Многое, что кажется нативным, давно в браузере: работа с файлами, геолокация, определение ориентации, доступ к камере для простых сценариев.
- Проверить официальные плагины Capacitor. Они поддерживаются вместе с ядром, а это половина проблемы.
- Проверить сообщество, но смотреть на дату последнего коммита. Плагин, который два года не обновлялся, сломается на первом же мажорном апдейте, и чинить его придётся вам.
- Только после этого писать свой.
Своя реализация оправдана, когда возможность критична для продукта и вы готовы её поддерживать. Плагин ради одной кнопки, которой пользуется полпроцента аудитории, - плохая сделка: написать его два дня, а тащить придётся годами.
Что стоит дорого
Не написание, а сопровождение.
Обновление Xcode и целевых SDK. Apple и Google регулярно поднимают минимальную версию SDK, под которую собирается приложение. Каждое такое поднятие - это пересборка всех нативных зависимостей и проверка, что ничего не отвалилось. Свежий Xcode может перестать собирать код, который компилировался вчера: изменились требования к синтаксису, устарел API, поменялись настройки подписи. Раз в год это гарантированно несколько дней работы, даже если в продукте ничего не менялось.
Разрешения и ревью App Store. Каждый новый системный доступ требует строки в Info.plist с человеческим объяснением: зачем вам микрофон, зачем фотогалерея, зачем контакты. Формулировка «для работы приложения» гарантированно получит отказ. Объяснение должно быть конкретным, и ревьюер проверяет, что заявленное совпадает с фактическим поведением. Доступ, который вы запросили «на будущее», - готовая причина отказа.
Расхождение поведения платформ. Один и тот же плагин на iOS и Android почти всегда ведёт себя по-разному в мелочах: порядок колбэков, поведение при отказе в разрешении, что происходит после сворачивания. Эти различия либо прячутся внутри плагина, либо всплывают в бизнес-логике, и лучше первое.
Тестирование. Нативную часть нельзя проверить в браузере. Нужны реальные устройства, минимум одно недорогое на Android и одно на iOS, а лучше несколько разных.
Когда плагины уже не спасают
Плагин закрывает точечную возможность. Он не поможет, если проблема в самой модели работы приложения.
Уходить в натив целиком стоит, если:
- приложение постоянно работает с железом в фоне: непрерывный GPS-трекинг, обмен по Bluetooth с устройством, обработка потока с камеры в реальном времени;
- интерфейс и есть продукт, и его отзывчивость - конкурентное преимущество: редакторы, инструменты для творчества, игры;
- нужна тяжёлая графика: 3D, AR, фильтры реального времени;
- больше половины экранов оказались нативными. Это уже нативное приложение, к которому зачем-то приделан WebView, и от гибрида остались одни накладные расходы.
Последний пункт - главный индикатор. Пока нативного кода меньше десятой части, гибридная архитектура работает и экономит деньги. Когда его становится половина, вы платите за оба подхода сразу.
Как я оцениваю проект с нативными частями, сколько занимает каждая возможность и что входит в поддержку после релиза - на странице кроссплатформенной разработки. Если по вашему сценарию видно, что гибрид не потянет, я скажу это до договора, а не на третьем месяце.
Ещё про приложения
3 статьи-
Capacitor, React Native или Flutter: чем платит бизнес за выбор
Flutter или React Native, а может Capacitor: экономия против натива у всех похожая. Разбираю, чем именно платит владелец приложения при каждом выборе.
38 показов в месяц у вопроса -
Что такое PWA и когда оно заменяет приложение
Что такое PWA простыми словами: сайт с иконкой на экране, офлайном и уведомлениями. Что умеет, чего не умеет на iOS и в каких случаях заменяет приложение за 350 000 ₽.
860 показов в месяц у вопроса -
Гибридное или нативное приложение: чем платишь за экономию
Разработка кроссплатформенных приложений на Capacitor: сколько экономит гибрид, где он неотличим от натива, где проигрывает и что ломается в реальности.
672 показов в месяц у вопроса




