В былые времена клиенты присылали цветные изображения документов со сканера по емейлу. Кто ленился или не мог — отправлял свой загранник по почте. С этим неплохо справлялось решение от ABBYY. Потом появились телефоны и люди стали присылать фотографии паспортов. ABBYY сломался. Перешли на решение от Smart Engines и даже с фотографиями, посланными через WhatsApp особых проблем не было. Но потом пошли умные мобилки с включенными по умолчанию водяными знаками типа «Xiaomi 9T» часто перекрывающими важнейшую часть паспорта. Вдобавок к этому наличие в кадре на фотографии пальцев и бликов тормозило общение с клиентом.
Когда паспорт распознается в видеопотоке на телефоне клиента, то этой проблемы так остро не стоит. Почему?
- Мобильное ядро отображает рамку документа в процессе распознавания, не просто маску поверх видео, а именно рамку документа, находящегося в руках. По моим ощущениям, это дает человеку понимание, где не должно быть его пальцев. Вообще, это интересная тема для отдельного исследования.
- Система использует механизм сатурации данными и готова выдать результат только после определенной уверенности в них.
Появилось желание перейти на мобильные приложения, чтобы клиент самостоятельно отправлял пакет документов менеджеру. Натив я не рассматривал из-за нежелания двух кодовых баз. Реакт-нейтив… послушал хвалебные слова тех, кто перешел с него на флаттер. Оставался маленький мостик соединения flutter и мобильного SDK от SmartEngines — у меня не было готовой интеграции. (На текущий момент интеграция уже есть)
Я решил написать небольшую заметку, поскольку сам бы просто мечтал о ней, когда вникал в эту тему.
О флаттере
Во Флаттере соединение с мобильной платформой существует в нескольких вариантах, можно собрать проект вместе с флаттером как зависимостью, но если вы делаете универсальное решение, которое может встраиваться в любой проект, конечно же подходят только плагины.
Работа с плагином мало чем отличается от обычной клиент-серверной архитектуры. Ты можешь инициировать запрос в нативную платформу, можешь ожидать ответ, можешь открыть канал и гонять данные туда-сюда постоянно. Простые структуры данных перекидываются из java в dart без проблем.
Все это описывает документация в разделе Platform Channels. Это хорошо покрытый примерами механизм общения между платформами. Проблем нет вообще. Однако, документация не описывает как работать, когда тебе нужно вызывать нативный UI внутри флаттера.
Плагин, по факту, это обычный flutter проект, с типичной для него структурой, только его можно подключить как зависимость.
Ковыряясь в устройствах подобных плагинов на pub.dev (работающих с OCR) я убедился, что не многие спешат переходить на Flutter Plugin Embedding API v2, которое уже существует как два года. Документация затрагивает только миграцию на новое API без примера .
Изучаем проекты
У меня не было опыта разработки на Андройде, задача усложнялась. Мне нужны рабочие примеры. На просторах сети мне все таки удалось найти доступный рабочий пример реализации Flutter Plugin Embedding API v2, а на pub.dev я распотрошил наиболее читаемый для меня плагин qrcode_scanner и обновил его api до свежей версии. Внизу ссылка на этот репозиторий.
Кстати, если вы испытываете страх, что не сможете писать код на нативной платформе в случае выбора флаттера (не умеете в натив), я вам хочу сообщить, что официальный репозиторий с плагинами покрывает огромное количество кейсов. У меня не было опыта в java и в андройде, но какой-нибудь плагин file-picker содержит в себе и работу с файловой системой и с API андройда. Подсмотреть есть где.
Остается скрестить два проекта: убрать вызов экрана распознавания qrcode и подставить экран распознавания документа от SmartEngines.
Как происходит общение dart-native
Как правило, для общения с нативным кодом используются два механизма:
MethodChannel — для простого обмена сообщениями dart-java.( Можно посылать и в обратную сторону.)
EventChannel — если нужно без остановки получать какие-то данные с платформы. Например, данные с акселерометра.
В первой версии Flutter Plugin Api плагины инициализировались один раз при старте приложения. А вторая версия API подразумевает, что каждый плагин создает свой отдельный инстанс и он теперь благодаря куче callback знает, когда его движок подключает и отключает.
Инициализация нового плагина без UI
class MyNewPlugin implements FlutterPlugin {
public MyNewPlugin() {
// Все классы плагинов для Android должны поддерживать no-args
// конструктор. .По умолчанию конструктор no-args не объявлен,
// но мы включаем его здесь для ясности.
// На этом этапе ваш плагин создаст instance, но он еще не привязан к Flutter.
// Поэтому здесь нельзя обращаться или к его ресурсам или пробовать им управлять.
}
@override
public void onAttachedToFlutterEngine(FlutterPluginBinding binding) {
// Ваш плагин привязан к FlutterEngine.
// Можно обращаться к движку через binding.getFlutterEngine()
// Или к BinaryMessenger с помощью
// binding.getBinaryMessenger()
// И контекст
// binding.getApplicationContext()
// А вот доступа к Activity здесь нет!
}
@override
public void onDetachedFromFlutterEngine(FlutterPluginBinding binding) {
// Ваш плагин отвязан от Flutter.
// Необходимо очистить все ресурсы и ссылки, которые
// созданы в onAttachedToFlutterEngine().
}
}
Инициализация нового плагина с UI
class MyNewPlugin implements FlutterPlugin, ActivityAware {
@override
public void onAttachedToFlutterEngine(FlutterPluginBinding binding) { ... }
@override
public void onDetachedFromFlutterEngine(FlutterPluginBinding binding) { ... }
@override
public void onAttachedToActivity(ActivityPluginBinding binding) { // ...
// Теперь плагин связан с Android Activity.
// Если вызывается этот метод, он всегда вызывается после
// onAttachedToFlutterEngine().
// Получить ссылку на Activity
// binding.getActivity()
// Жизненный цикл
// binding.getLifecycle()
}
@override
public void onDetachedFromActivityForConfigChanges() {
// Плагин уничтожен из-за изменений конфигурации. Она будет возвращена.
//Ваш плагин должен очистить все ссылки и связанные ресурсы.
}
@override
public void onReattachedToActivityForConfigChanges( ActivityPluginBinding binding ) {
// Плагин теперь связан с новым экземпляром Activity
// после изменения конфигурации. Теперь вы можете восстановить
// ссылку на Activity и связанные с ней ресурсы.
}
@override
public void onDetachedFromActivity() {
//Плагин больше не связан с Activity.
// Нужно очистить все ресурсы
}
}
В случае плагинов, предназначенных только для UI, ваш код должен размещаться в событии onAttachedToActivity().
В onDetachedFromActivity() необходимо деактивировать ваши созданные объекты или ссылки.
В onAttachedToFlutterEngine() что-то делать нет никакой необходимости. Это нормально для UI плагинов.
Тут подробнее https://medium.com/flutter/modern-flutter-plugin-development-4c3ee015cf5a
По факту интеграция нативного проекта с примером оказалась простой. В нативный проект добавляется 1 новый файл QrscanPlugin.java где мы слушаем вызов из дарта, чтобы открыть экран с камерой для распознавания.
OCR движок может вернуть и проективный кроп паспорта.
Есть одна очень полезная штука в OCR движке — паспорта могут возвращаться с компенсированными геометрическими искажениями. Как будто-то со сканера. Эти паспорта частенько печатаются на бейдж клиенту. С подобного рода технологиями для меня должен быть визуальный контакт — некоторое наработанное чувство, которым я потом могу оперировать, чтобы чувствовать уверенность в технологии. Также, мне нужно понимать, какого качества заводятся документы в систему.
Передача изображений native — dart
В андройде не рекомендуется передавать между Activity данные больше 1mb (можно упасть по памяти) поэтому изображение паспорта можно сохранить в темпы приложения, получить на него Uri, вернуть обратно в dart с общим json. Но если вы не собираетесь передавать такой объем, можно запаковать изображение в base64 и отдать как строку в дарт.
По моим наблюдениям, через base64 все работает даже быстрее. Вероятно, операции с файловой системой чуть более сложны, но разница не сказать, что так уж заметна на глаз.
О чем еще следует сказать
Flutter Camera plugin
https://pub.dev/packages/camera
Первой моей мыслью было использование этого плагина, чтобы реализовывать OCR внутри дарта. Почему бы нет? Одна кодовая база будет же! Плюсовые библиотеки в дарт заводятся, а там… А там вроде бы даже пункт в описании есть «Add access to the image stream from Dart.» Но этот интерфейс не покрывает документация. А в разделе Issue есть тикет посвященный экспериментам. https://github.com/flutter/flutter/issues/26348
Пообщавшись с разработчиками, я понял что это все равно вариант с лишним оверхедом, а OCR на мобилке штука ой дорогая!
PlatformView
Это еще один способ интеграции нативных экранов. Так сделан сделан популярный плагин QRcode. У него в описании значится отказ от открытия активити как преимущество. Честно говоря, мне не удалось выяснить почему. Есть предположение, что из-за того что рендер происходит внутри композиции флаттера, эффекты fade-in и fade-out работают гладко. Это действительно эстетично, пользователь не видит того внезапного резкого появления камеры, а видит анимацию перехода. Но решения на PlatformView могут вызывать проблемы с производительностью и есть некоторые ограничения.
Отладка
Для отладки вам необходимо открывать папку «project/example/android» для работы с плагином. В этом случае у вас IDE найдет все импорты.
Заключение
Для меня флаттер решает одну интересную задачу (впрочем, как и реакт-нейтив) — он позволяет разработчику быть самостоятельным в своем творчестве. Если ваш код не летит в одни ворота, а летит сразу в несколько — это позволяет реализовывать идеи вне команды, которая, как правило, существует благодаря бизнесу. Это означает, что вы можете увеличивать социальное благо для общества, будучи независимым от него. Не могу не поддерживать такого рода инструменты.
Ссылка на демо проект
https://github.com/Alexufo/Flutter-ActivityAware-plugin-example