Нативный код во Flutter проекте
Всем привет! Я iOS/Flutter-разработчик в DexSys на проекте Dexbee. В этой статье поделюсь своим опытом подключения нативного кода на мобильных платформах Android/iOS во Flutter проекте «DexBee Клуб».
DexBee - это система мотивации и повышения вовлеченности в занятия фитнесом, основанная на контроле нагрузки во время тренировки. А «DexBee Клуб» - приложение для управления клубной системой вовлечения клиентов в фитнес. Позволяет протестировать инфраструктуру в клубе на соответствие требованиям оборудования DexBee, а также запускать соревнования в клубе. Первый релиз приложения был в конце 2021 года, и работа над ним до сих пор продолжается.
DexBee - это система мотивации и повышения вовлеченности в занятия фитнесом, основанная на контроле нагрузки во время тренировки. А «DexBee Клуб» - приложение для управления клубной системой вовлечения клиентов в фитнес. Позволяет протестировать инфраструктуру в клубе на соответствие требованиям оборудования DexBee, а также запускать соревнования в клубе. Первый релиз приложения был в конце 2021 года, и работа над ним до сих пор продолжается.
Так выглядит приложение «DexBee Клуб» внутри
В основном, когда вы пользуетесь Flutter, нативный код писать нет необходимости. Разработчиками этого фреймворка и просто крутыми энтузиастами сейчас написано множество библиотек, которые, в свою очередь, сами имеют пару-тройку вариаций. Да и пригождается он только в том случае, когда нужно обратиться к возможностям железа телефона, например к:
- датчикам, камере, аккумулятору, геолокации, звуку, подключению
- обмену информацией с другими приложениями, запуску других приложений
- сохраненным настройкам, специальным папкам, информации об устройстве и так далее
Но, как я написал ранее, всё это уже реализовано.
Второй случай, при котором нам все-таки надо писать нативный код, это необходимость в «нестандартной» фиче, которую мне и надо было реализовать.
По моему опыту, в жизни каждого разработчика наступает момент, когда вы связываетесь с китайцами и появляется задача включить в проект их самописный фреймворк. Естественно, он написан на нативных языках, Obj-C/Java.
Вкратце, основная задача:
Есть оборудование, которое клиенты должны суметь настроить, не устанавливая приложения поставщиков этого самого оборудования. Настройка происходит по Bluetooth, где, описывая каждую команду, на приём/отправку пакетов ушёл бы не один месяц. Поэтому, заботливые поставщики написали фреймворки-обёртки под мобильные платформы iOS и Android.
Но наш проект — на Flutter. Я думал, что придётся немного «попотеть», но, на деле, все оказалось намного проще, чем вспоминать свои навыки написания под Андроид трёхлетней давности.
Задача ясна, пора начинать делать
Стоит упомянуть, технологии мы стараемся держать самыми актуальными, поэтому Flutter версии 3.3.10. А описывать я буду задачу на передачу команды «Сканировать доступные для подключения WiFi сети» оборудованию DexBee.
На выход нам нужен список сетей для презентации пользователю. На основе этого списка пользователь будет решать: к какому WiFi необходимо подключить устройство. Код будет предоставлен на Flutter и Swift, чтобы не получить в свой адрес больших и малых лепёх от уважаемых андроид-разработчиков.
Итак, приступим
Для начала, нам необходимо найти нужный метод во фреймворке, какие параметры он получает на вход и что отдаёт на выходе. Подключаем фреймворк в автоматически созданные флаттером нативные проекты и изучаем.
Для получения ответа от устройства подписываемся на делегат HubConfig.
Находим нужный метод и описываем работу с ним.
Что мы имеем в результате: массив из Any. Это значит, что на выходе мы можем получить буквально что угодно.
Забыл упомянуть, что также у нас имеется документация по работе с фреймворком... на китайском. В современном мире это уже не такая большая проблема — словарь по китайскому языку покупать не пришлось. Воспользовавшись переводчиком, я узнал, что на выходе мы получаем словарь, где по ключу можно взять нужную мне информацию для отображения пользователю. Углубляться в это не буду, так как моя статья всё же о другом.
В результате поиска в документации мы получили объект WiFiHubModel, который хранит в себе одно значение — имя сети.
Теперь мы имеем всё необходимое для написания, собственно, кода на Flutter.
В результате поиска в документации мы получили объект WiFiHubModel, который хранит в себе одно значение — имя сети.
Теперь мы имеем всё необходимое для написания, собственно, кода на Flutter.
Стилизованный код на gist: https://gist.github.com/Wenomok/98b40e90e6095cf42d266053fe2fa930
По порядку:
Просто до невозможности. А ведь первой моей мыслью по этой задаче было — «сто пятьсот часов поди потребуется…».
Если копать чуть глубже, то общение по каналу происходит асинхронно по типу «отправил запрос – получил ответ», причём ответ точно должен быть хотя бы null. При вызове метода invokeMethod у MethodChannel идёт передача названия вызываемого метода платформы и его аргументы
По порядку:
- Так как помимо нашей маленькой библиотечки есть ещё куча других библиотечек, которые также пользуются нативными фичами платформы, то нам необходимо создать канал с уникальным идентификатором, по которому и будет происходить «общение» флаттер кода с нативом. Уникальным же он должен быть для того, чтобы не отправить запрос в натив другой нашей библиотечке и не получить совершенно другие данные на выходе.
- По нашему каналу отправляем запрос в нативный код: выполнить операцию с названием scanWiFiHubList, ожидая на выходе словарь.
- И мапим словарь в нужный нам объект
Просто до невозможности. А ведь первой моей мыслью по этой задаче было — «сто пятьсот часов поди потребуется…».
Если копать чуть глубже, то общение по каналу происходит асинхронно по типу «отправил запрос – получил ответ», причём ответ точно должен быть хотя бы null. При вызове метода invokeMethod у MethodChannel идёт передача названия вызываемого метода платформы и его аргументы
Далее, эта информация преобразуется в бинарник и отсылается на платформу.
Канал платформы(MethodChannel) — это объект, который объединяет имя канала и кодек для сериализации/десериализации сообщений в двоичную форму и обратно.
Далее следует сам нативный код. На флаттере был открыт канал с идентификатором, надо на него подписаться. Чтобы разделить логику разных фич, создаём отдельный класс и в нём подписываемся на канал. Идентификатор должен соответствовать. А делаем мы это на инициализации приложения, то есть в AppDelegate.
Канал платформы(MethodChannel) — это объект, который объединяет имя канала и кодек для сериализации/десериализации сообщений в двоичную форму и обратно.
Далее следует сам нативный код. На флаттере был открыт канал с идентификатором, надо на него подписаться. Чтобы разделить логику разных фич, создаём отдельный класс и в нём подписываемся на канал. Идентификатор должен соответствовать. А делаем мы это на инициализации приложения, то есть в AppDelegate.
стилизованный код на gist: https://gist.github.com/Wenomok/ea1cf6775ade93e8df2d150eb3bf2557
Разберемся. При создании AppDelegate, мы также создаем объект HubMethodChannel и при запуске приложения вызываем у него метод handle. В handle создаем канал с тем же идентификатором, что и во флаттер, и подписываемся на обновления этого канала с помощью метода setMethodCallHandler. У нас только один метод и его название scanWiFiHubList, поэтому ждем его вызов и выполняем нужные нам действия.
В нем передаем замыкание с двумя параметрами: call и result. Call — объект, который приходит с флаттера, result — то, что должно вернуться во флаттер.
Разберемся. При создании AppDelegate, мы также создаем объект HubMethodChannel и при запуске приложения вызываем у него метод handle. В handle создаем канал с тем же идентификатором, что и во флаттер, и подписываемся на обновления этого канала с помощью метода setMethodCallHandler. У нас только один метод и его название scanWiFiHubList, поэтому ждем его вызов и выполняем нужные нам действия.
В нем передаем замыкание с двумя параметрами: call и result. Call — объект, который приходит с флаттера, result — то, что должно вернуться во флаттер.
Что имеем в итоге:
- интуитивно понятная реализация фичи с необходимостью внедрения нативных фреймворков платформы
- довольные клиенты, которые теперь без постронней помощи могут настроить своё оборудование на работу с нашими сервисами
Flutter не ограничивает разработчиков на пользование фичами, поставляемыми операционной системой. Конечно, при релизе новой версии операционки нужно будет подождать, когда новоиспеченная фича появится и на флаттере. Но реализовать её так просто, что энтузиасты делают это чуть ли не в первую же неделю после релиза ОС.
Для более подробного изучения этой темы я советую ознакомиться с официальной документацией: https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-swift-tab
А также могу посоветовать хорошую статью на Хабре о том, как это работает: https://habr.com/ru/articles/666272/