Kotlin Multiplatform / Опыт работы с Kotlin Multiplatform за 10 месяцев

Опыт работы с Kotlin Multiplatform за 10 месяцев

cover.png

После участия в meetup с докладом про Kotlin Multiplatform мы в IceRock Development решили рассказать немного истории нашего опыта с этой технологией. Начали пробовать работать с kotlin multiplatform в июле 2018 года с технодемки, далее в августе уже начали делать первый боевой проект, в сентябре он уже вышел в стор. На данный момент 6 проектов выполнены, 1 эксперимент с mpp заморожен и 2 проекта в разработке прямо сейчас.

NFC TAG

Небольшой проект (4 экрана, 2 запроса к серверу, локальное хранение файла, считывание QR и NFC) на две платформы. На нем впервые попробовали multiplatform и Kotlin/Native. Делался он во времена до kotlin 1.3 (то есть до бета версии Kotlin/Native).

Чего мы хотели

Платформенная сторона (swift, kotlin) должна содержать:

  • UI;
  • Привязку к viewModel’ям из общей библиотеки;
  • Конфигурацию для фабрики компонентов общей библиотеки (передаем URL сервера с кем работаем, передаем преференсы);
  • Полностью платформенные вещи — работа с камерой, пермиссиями и NFC метками.

Общая библиотека не должна добавлять проблем платформенным разработчикам (айосник не должен иметь трудностей при компиляции из-за наличия общей библиотеки, как и андроидщик).

Что смогли

Все что хотели и даже чуть больше — в общей библиотеке сразу сформировался набор компонентов для переиспользования в других проектах.

Разработка шла в первую очередь со стороны ios-разработки (и UI делался, и айосник разрабатывал на kotlin общую логику), а после завершения разработки iOS за 3 рабочих дня было сделано android приложение которое без проблем прошло тестирование (вся общая логика уже была отлажена iOS).

С чем столкнулись

  • Неизвестность — мы вышли с привычного мира android с retrofit, rxjava, gson и ios с alamofire, rxswift, objectmapper и попали туда где не знаем и не находим аналогов этих библиотек. Простые задачи которые решались за 5 минут на привычных платформах стали требовать много времени. Сначала мы начали прописывать свои expect/actual классы для работы с сервером, чтобы на платформах остался retrofit/alamofire и парсинг. Но чуть позже наткнулись на ktor (http клиент) и вовремя вышла рабочая версия клиента для обеих платформ, что сильно упростило нашу задачу. Вслед за ktor, который заменил retrofit и alamofire, мы нашли kotlinx.serialization, который только-только получил поддержку ios таргета, сильно ограниченную, но позволяющую делать главное, что нам нужно было — парсить json. Под конец оставалось только найти аналог rxJava/rxSwift и заменой оказались coroutines, которые в тот момент тоже только начали адекватно работать на ios, хоть и без многопоточности (и так до сих пор);
  • Библиотеки с поддержкой iOS сильно завязаны на версию компилятора, которой были собраны. Из-за этого требовалось аккуратно подбирать версии библиотек, которые совместимы между собой (а они еще и связаны внутри себя друг с другом бывают). Эта проблема остается и сейчас, поэтому советую определить проверенные совместимые друг с другом версии библиотек и котлина, после чего не обновлять их без полноценного тестирования всего приложения (могут быть помимо проблем на этапе компиляции еще и проблемы в рантайме);
  • Gradle для айосников — привыкшие к xcode и cocoapods айосники на старте потратили заметно времени на ознакомление с работой gradle, как добавлять новые зависимости, запускать компиляцию нужного проекта и фреймворка. Основную часть времени проблемы были с несовпадением версий библиотек и компилятора, ошибками самого kotlin/native компилятора (что на данный момент уже не встречается у нас в работе), а из-за того что ребята gradle видили впервые им не сразу становилось понятно что проблема не в их настройке, а в библиотеках/компиляторе;
  • Многопоточность в Kotlin/Native сильно отличается от многопоточности в kotlin-jvm. Это создает проблемы для реализации многопоточности в coroutines (issue). В результате у нас сейчас используются корутины на главном потоке, а запросы к серверу ktor делает через системные инструменты, которые результат выдают в callback на главный поток. Это создает проблемы если есть тяжелые операции, которые хотелось бы вынести на фоновый поток — тут придется отказаться от использования корутин и использовать свои expect/actual функции, с реализацией в ios на основе Worker’ов (абстракция для запуска потоков) и вручную прописывать логику передачи объектов между потоками (главная особенность многопоточности в K/N — только 1 поток может изменять какой либо объект. Читать могут все потоки, только если объект заморожен). Подробнее тут — Concurrency. Хоть и звучит страшно — но делать приложения это не мешает;
  • Generic’и затираются при компиляции в iOS, из-за чего в swift коде появляются форскасты. На данный момент уже влиты в основную ветку kotlin/native изменения kpgalligan/generics содержащие базовую поддержку дженериков (без in и out поддержки), будет доступно в 1.3.40 котлине;
  • Kotlin/Native не поддерживает variadic аргументы в objc функциях, из-за чего пришлось переносить вызов String(format: arguments:) на сторону swift’а, оставив в kotlin только интерфейс для вызова. На данный момент уже готово решение проблемы и будет доступно всем в 1.3.40;
  • Suspend функции не попадают в хидер ios фреймворка, так как в objc отсутствует такая конструкция как suspend функция. Поэтому в публичном интерфейсе общей библиотеки у нас запрещено использование suspend функций, если требуется вынести на нативную часть suspend функцию то для айос делается преобразование suspend в вызов с калбеком через suspendCoroutine, в который передается Continuation и именно он передается в objc/swift;
  • Работа с ресурсами между платформами различается. Например, строки локализации — на андроиде через контекст требуется по id получить строку и во время жизни приложения по этому id можно получить разные строки для разных контекстов — если, например, язык пользователь переключил или строки зависят от размера экрана. А на айосе локализованную строку можно получить в любом месте приложения просто по строковому ключу и во время жизни приложения они никогда не меняются — при смене языка системы все приложения перезапускаются. Для корректно работы на обеих платформах мы выделили StringResource (для идентификаторов строк — на android это Int, на ios это String) и добавили абстракцию StringDesc — это sealed class содержащий строку или StringResource и только на платформе из абстракции мы получаем конечное значение — строку для вывода в UI. Для удобного хранения идентификаторов ресурсов мы выделили expect class MR — по аналогии с андроидным R.

Выводы

Технология хоть и сырая, но использовать можно, наши наработки уже позволяют избежать всех тех проблем, на которые мы наткнулись на старте, продолжаем развивать направление.

VEKA Замерщик

Уже давно находящийся в сторах проект, с множеством уже реализованного функционала, написанный нативно для iOS (Swift + ObjC) и Android (Kotlin + старый код на Java). Для нового этапа в котором добавляется очень много работы с сервером, было принято решение выделить логику работы с сервером в общую библиотеку на kotlin.

На этом проекте мы уже использовали kotlin 1.3 с бета версией kotlin/native.

Чего мы хотели

  • Модели сервера, api работы с сервером и репозитории для обращения к данным написать 1 раз — на kotlin, а дальше использовать в swift и kotlin коде на ios и android.

Что смогли

Все что хотели — удалось.

С чем столкнулись

  • Много разных проблем с ktor на ios — апи была не из простейших, тут были и формы и загрузка файлов, с multipart загрузкой на андроиде все было решено быстро и просто, но на айос возникли проблемы и пришлось сделать на тот момент загрузку на платформенной стороне (через alamofire). На данный момент возможно проблемы с multipart в ktor-client уже нет, но мы не проверяли;
  • Не все классы, которые используются в общей библиотеке доступны в header’е iOS фреймворка, когда используется разбиение на модули самой библиотеки. У нас общая библиотека проекта зависит от SCL (standart components library) — наша внутренняя библиотека общих компонентов. И при компиляции iOS фреймворка мы обнаружили что некоторые классы из SCL пропали в хидере фреймворка. Чтобы они были в хидере, требовалось упомянуть эти классы в публичном интерфейсе самой библиотеки, которая компилируется в фреймворк. Мы это делали простым объявлением глобальной опциональной переменной с этим классом, которую сразу выставляли в null. Компилятор видел в публичном интерфейсе нужный нам тип и генерировал его в хидере. На данный момент это не требуется — можно использовать export для нужных зависимостей;
  • Параллельно работая над общей библиотекой, айосник и андроидщик периодически ломали компиляцию общего модуля на другой платформе. Так как мы используем git submodule для подключения общей библиотеки, то разработчик не получит нерабочий модуль случайно — только когда он обновляет состояние, но все равно получить некомпилируемый модуль после подтягивания изменений было неприятно. Вариант решения был — запускать на каждый пуш изменений общего модуля на билдмашине сборку, чтобы провалидировать что общий модуль собирается. Но проблема со временем прекратилась (ребята засинхронизировались что может ломать соседа) и решение не применили;
  • Ограниченная поддержка @Serializable. В Kotlin/Native сериализация не поддерживала наследование классов и использование enum classes. Про это не знал андроидщик, когда реализовывал сериализацию, на андроид все было хорошо, но как только это подтянул айосник — мы уткнулись в ограничения библиотеки. Пришлось переделать код, убрав enum classes и наследование. На данный момент enum classes уже поддерживаются и на айос;
  • Айос и андроид приложения написаны с разным подходом и имеют разные структуры данных, обобщенный вариант серверного взаимодействия потребовал от обеих платформ некоторой переделки существующего кода, чтобы работало с данными общего модуля, а не старыми данными от самой платформы;
  • Обновление kotlin 1.3 хорошо стабилизировало инфраструктуру (с библиотеками стало заметно меньше проблем, обновлять библиотеки стало менее рискованно, но тест все равно требовался на обеих платформах из-за возможных рантайм проблем).

Выводы

Технология позволяет подключать общий код даже к старым проектам, никаких препятствий для шаринга кода не добавляется. Но внедрять это в существующие проекты нужно с пониманием, что потребуется обобщать логику и типы данных между платформами. Технология уже не ощущается сырой и проблем вызывает меньше. Проект доступен в PlayMarket и AppStore.

RUNNERS

Уже готовый проект, отдельно на iOS и Android, с сложным серверным апи (оно плохо документировано и доставляет сильные проблемы при реализации на клиенте из-за неожиданных деталей, выясняющихся “находу” при тестах).

Чего мы хотели

Выделить уже написанную логику клиент-серверного взаимодействия, rest DTO и репозитории в общую библиотеку на kotlin для упрощения дальнейшей работы с API.

Что смогли

Выделили код клиент-серверного взаимодействия, сущности и репозитории с андроида в общий модуль, но интеграция в айос оказалась слишком трудозатратна и была заморожена.

С чем столкнулись

  • На бета версиях ktor совсем сломался компилятор kotlin/native выдавая непонятные ошибки, решилось все только после релиза новой беты которая исправила проблему;
  • Между уже существующими айос и андроид реализациями была очень большая разница, из-за чего то что мы выделили из андроида не подошло к айос приложению и требовалось многое переделать уже в айос.

Выводы

Если требуется выделить в общий модуль уже существующий функционал в нативных приложениях, то следует сперва оценить на сколько схожие подходы используются в обеих платформах и быть готовым к большой переделке при интеграции общего модуля.

Apatris

Новый проект для разработки с нуля, с планами на андроид и айос, но старт только с айос разработки. Было решено раз в планах обе платформы — сделать с использованием mpp, оставляя на платформенной стороне только UI. Проект представляет собой онлайн кошелек криптовалюты, с множеством взаимодействия с сервером, но без оффлайн БД.

Чего мы хотели

Ведя разработку только айос приложения получить готовую (или почти готовую) к использованию на андроид библиотеку, которая будет иметь вьюмодели, репозитории, работу с сервером и все прочее не UI.

Что смогли

Все что хотели удалось, библиотека почти готова к использованию на андроид — не хватает только нескольких actual реализаций (около 4 классов).

С чем столкнулись

  • Так как мы работали с деньгами, при чем криптой в которой 0.00000000001 это нормальное дело — потребовалось использовать BigDecimal, которого котлин не предоставляет в stdlib. Поэтому сделали свои expect/actual реализации (используя для айос NSDecimalNumber внутри);
  • Компиляция Kotlin/Native очень медленная, в сравнении с компиляцией под андроид. У Kotlin/Native пока нет инкрементальной компиляции и единственное что помогает у нас собирать относительно быстро — сильная модульность. Модули компилируются в KotlinLibrary, а потом уже все библиотеки соединяются в большом модуле компилируемом в ios framework. Но все равно это оказало замедляющий эффект в сравнении с разработкой через андроид платформу.

Выводы

Запускать проекты на mpp даже если на старте только айос платформа — возможно и не накладывает заметного оверхеда на разработку (при имеющихся наработках и опыте), дальнейший запуск второй платформы будет сильно проще и бизнеслогика уже отлажена. Проект успешно вышел в AppStore без проблем на ревью.

BeGreatApp

Тоже новый проект для разработки с нуля, также с планами на андроид и айос, но старт только с андроид разработки. Вся новая андроид разработка у нас выполняется с использованием мультиплатформы, так как не накладывает рисков (это привычный для андроида котлин и jvm), а в дальнейшем может дать ускорение при выпуске второй платформы. Проект содержит работу с сервером, синхронизацию данных с локальной базой данных, порядка 10 экранов и ин-аппы.

Чего мы хотели

Получить общую библиотеку со всей логикой до уровня вьюмоделей включительно, которую потом можно будет использовать в айос.

Что смогли

Общая библиотека содержит все, кроме реализации БД. БД было вынесено как expect/actual класс и реализуется для каждой платформы отдельно с использованием Room под капотом на андроиде. Для компиляции под айос потребуется реализация БД.

С чем столкнулись

  • База данных — нет поддержки мультиплатформы у привычных Room и Realm, из-за чего пришлось сделать expect/actual класс по работе с БД. На данный момент уже есть мультиплатформенная реализация sqldelight. Realm на данный момент только собирают информацию о количестве заинтересованных поддержкой Kotlin/Native и kotlin mpp.

Выводы

Все прекрасно, но в следующий раз попробуем sqldelight для переноса работы с базой данных в общий модуль. И так и нет информации о сильных ORM для мультиплатформы (я в курсе только о sqldelight и еще более низкоуровневый SQLiter который просто предоставляет доступ sqlite в mpp). Проект успешно выпущен в Play Market.

COFFE

Большой проект с нуля на две платформы одновременно с функционалом карты, реалтайм обновления данных на карте (движение машин), меню товаров, корзина, профиль с управлением адресами на старте.

Чего мы хотели

Получить общую библиотеку со всей логикой до уровня вьюмоделей включительно, получить готовый модуль для работы с сокетом в мультиплатформе, который мы сможем переиспользовать.

Что смогли

Всего желаемого добились, модуль по работе с сокетом сделали, под капотом используется SocketIo библиотеки для java и swift.

С чем столкнулись

  • При реализации сокетов — у SocketIo немного разный интерфейс работы на java и swift, из-за чего пришлось пойти на несколько допущений (в основном касается стандартных событий и ошибок);
  • При реализации ios варианта библиотеки сокета хотели подключить swift вариант SocketIo напрямую к котлину и в самом котлине всю реализацию сделать, но используемая версия котлина не поддерживала enum forward declarations которые были в хидере фреймворка, из-за чего пришлось делать интерфейс для платформы и реализовывать интеграцию в swift. С версией котлина 1.3.20 эта проблема уже решена;
  • Абстрактные классы после компиляции в ios становились просто классами (там нет понятия абстрактного класса), из-за чего компилятор не мог подсказать что какой-то метод должен быть реализован. Потребовалось добавить правило “в публичном интерфейсе общей библиотеки не должно быть абстрактных классов — вместо них нужно использовать интерфейсы”;
  • Для удобства андроидщиков потребовалось добавить поддержку Parcelize в общий модуль. Это очень просто решилось через expect/actual;
  • Странная работа iOS с Iterable — если мы в публичном интерфейсе общей библиотеки используем Iterable то в iOS он превращается в Any. Поэтому вместо Iterable используем более точные типы — List,Set и прочие;
  • Нужна была корректная работа с временем, таймзонами и форматирование дат и времени для UI. Для этого использовали библиотеку klock с некоторыми доработками для корректной работы по таймзоне девайса.

Выводы

Отладка большого проекта при использовании mpp была ощутимо быстрее — мы не получали расхождений между реализациями платформ, не исправляли проблемы дважды на двух платформах, в сравнении с отдельными разработками на айос и андроид отладка такого проекта прошла раза в 2–3 проще (по ощущениям).

EDUCATION

Средний проект с множеством справочной информации, чатом и видео трансляциями. Сразу на две платформы, в сжатые сроки, с дизайном на 80% состоящим из вертикальных списков.

Чего мы хотели

Сначала перевести на современный kotlin-multiplatform плагин (использовали все это время kotlin-platform-common/android/native). Помимо выделения общей логики и вьюмоделей в общую библиотеку — вынести и частично описание UI для быстрого построения экранов прямо из общей библиотеки, описывая “структуру экрана”. А так же сделать кодогенерацию ktor-client на основе swagger.

Что смогли

На kotlin-multiplatform плагин переехали, IDE стала индексировать ios specific код. Общая библиотека включает в себя все что не UI и дополнительно имеет Widget’ы — описания UI элементов, из которых далее платформы создают итоговые view. Таким образом экраны созданные из виджетов полностью контролировались из общего кода и не требовалось делать изменение в двух платформах. Кодогенерация тоже была успешно реализована и теперь переиспользуется. Так же в процессе работы выделили мультиплатформенные модули работы с пермиссиями и медиа пикерами.

С чем столкнулись

  • При реализации отправки фото по api столкнулись с багом на айос в kotlinx-io использующемся ktor-client’ом — из-за бага большой контент (5 мегабайт) очень долго преобразовывался в байты. Бага заведена, в ней нет пока решений, но как сообщил Николай Иготти (с ним обсуждал эту багу когда пытался решение найти) — ребята нашли внутри компилятора баг, так что возможно в 1.3.40 проблема больше не воспроизведется. Мы же применили обходное решение — преобразуем строку в байты не через kotlinx-io а через свою expect/actual реализацию использующую NSData для ios;
  • Дублирование логики работы с пермиссиями и получением изображений с камеры/галлереи — в итоге сделали свои мультиплатформенные модули которые теперь переиспользуем для простой и удобной работы без дублирования (работаем через корутины прямо из вьюмодели).

Выводы

Кодогенерация rest клиента из swagger удобна и ускоряет работу при изменяющемся API сервера, идея виджетов для частичного переноса управления UI в общую библиотеку хорошо себя показала и будет дальше развиваться, контроль пермиссий и пикеров с вьюмоделей еще больше упрощает разработку и убирает двойные затраты. Отладка проекта прошла очень активно и исправления, обновления поставлялись сразу на обе платформы.

UBER-LIKE (in progress)

Проект аналог убера, с такси катающимися на карте, заказом такси и подобным. Разработка с нуля на обе платформы сразу.

Чего мы хотим

Развить дальше концепцию виджетов, унеся больше общего UI в общий код (не теряя нативный UX — весь интерфейс должен быть нативным и привычным пользователю, построенным из платформенных view с подходами привычными на этой платформе). Выделить работу с картой в отдельный мультиплатформенный модуль. Разделить все функции приложения на отдельные мультиплатформенные модули внутри mpp общей библиотеки.

С чем уже столкнулись

Пока все идет хорошо и никаких новых препятствий не нашли.

FREELANCE (in progress)

Проект с нуля для двух платформ в планах (на старте только андроид) с разбиением на приложения клиента и исполнителя, функционал создания заявок, принятия заявок, выполнения, личный кабинет исполнителя и клиента. Большая часть UI построена из вертикальных списков.

Чего мы хотим

Развивать концепцию виджетов, добавить в библиотеку виджетов построение больших форм заполнения данных с поддержкой валидации. Разделить все функции приложения на отдельные мультиплатформенные модули внутри mpp общей библиотеки.

С чем уже столкнулись

Пока все идет хорошо и никаких новых препятствий не нашли.