Kotlin код в мультиплатформенной разработке. Инсайты Android-разработчика
Привет! Я — Вадим Савченко, Android developer в NIX.
Коллеги наверняка знают: переиспользовать код — обычная практика для любого программиста. Этот подход ускоряет процесс разработки и уменьшает вероятность ошибок. Когда видишь лаконичный код, лучше фокусируешься на бизнес-логике продукта. Именно эту цель мы с командой преследовали, когда впервые взялись за Kotlin Multiplatform Mobile. На конференции NIXMulticonf я презентовал результат нашей работы. А в этой статье подробнее расскажу, как использовать код для нескольких целевых платформ и почему знание Kotlin — ценный навык.
Как часто водится на аутсорсе, заказчик хочет готовое решение быстро, качественно и от одного разработчика. Kotlin Multiplatform Mobile (далее — КММ) экономит время и усилия и помогает достигнуть желаемого результата. Команда NIX давно успешно использует Kotlin в коммерческих проектах. На этот раз мы решили пойти дальше и узнать, какие возможности дает КМM.
Суть подхода заложена в слогане на официальном сайте КММ: Save time and effort by writing the business logic for your iOS and Android apps just once. Мы можем создавать модули с общим кодом и подключать их к разным нативным приложениям на Android и IOS.
Однако «чистый» Kotlin не так прост, как кажется. Нетрудно отказаться от Android импортов в слое бизнес-логики, но гораздо сложнее свыкнуться с мыслью, что Java импортов тоже не должно быть. Мы посмотрели на KMM в разрезе Clean Architecture. Здесь есть явное отделение слоя бизнес-логики от остальной части приложения (слой Domain). Разбиение на слои мы сделали с помощью модулей. Так проще проконтролировать, чтобы лишний импорт и платформозависимый код не попали, куда не следует. Получили три модуля — Presentation, Domain, Data:
- Presentation — это модуль с презентационной логикой и вьюшками;
- Domain — бизнес-логика;
- Data — репозитории и датасорсы.
Domain и есть наш KMM модуль, написанный на «чистом» Kotlin. Собрать его без проблем можно под IOS и Android. Presentation и Data — платформозависимые модули и не могут быть переиспользованы... Но это не точно. Мы решили выяснить, могут ли Data и Presentation стать KMM модулями на основе Pure Kotlin.
Сначала из Presentation выделили еще один модуль UI и закинули в него Views, Activities, Fragments. В Presentation же оставили MVP контракты и реализации презенторов. Зачастую в них можно обойтись без Android импортов. Затем из Data выделили Infrastructure. Здесь у нас DataSources и реализации Repositories, требующие Java или Android импортов. В Data остаются модельки, которыми оперирует приложение, и контракты Repositories. В итоге наши догадки оправдались: Data и Presentation тоже готовы к переиспользованию.

KMM позволяет сделать мультиплатформенными Infrastructure и View, но с ними дела обстоят сложнее. Создадим еще один простой KMM модуль для логирования. После этого вместо привычного java -> main у вас появится commonMain. androidMain и iosMain. Как же их использовать?


//Common
expect class MultiLogger() {
fun logError()
}
//Android
actual class MultiLogger {
actual fun logError() {
Log.e("ERROR", "ERROR ANDROID")
}
}
//IOS
actual class MultiLogger {
actual fun logError() {
println("ERROR IOS")
}
}
В commonMain лежит «чистый» Kotlin код. В случае с Domain весь код будет лежать здесь, а androidMain и iosMain нам вообще не понадобятся. Так как мы хотим, чтобы для Аndroid логирование происходило через стандартный Log, а не println(), нам нужны androidMain и iosMain. Для реализации в commonMain создали класс MultiLogger и обозначили его ключевым словом еxpect. В androidMain объявили класс MultiLogger и реализовали лог с помощью привычного нам Log. В iosMain используем println и надеемся, что он нас устроит :).
По аналогии с логером то же самое можно сделать с UI модулем и Infrastructure. Задача такого разбиения — отделить то, что может быть собрано под Android и IOS и в будущем легко реализовано на каждой из этих платформ. Если не хотите вручную писать View отдельно для двух платформ, воспользуйтесь готовыми решениями. Например, moko-widgets. Так же и с Data слоем, есть библиотека moko-permissions, которую можно взять для ваших репозиториев и не прибегать к expect и actual.
О чем нам никто не сказал
Когда мы впервые столкнулись с Kotlin, нас ждало несколько сюрпризов. Расскажу вам о них, чтобы вы были ко всему готовы :)
Из минусов — мультиплатформенные проекты требуют новейшей версии системы сборки Gradle. Переход к старым проектам затрудняется. Если вы уже переключились на мультиплатформу, но через какое-то время вам надо вернуться в прошлый коммерческий проект, сделать это будет сложно. Gradle вечно что-то кеширует, приходится постоянно чистить кеш, но чаще это не помогает. Ок, переустанавливаем. Но на все это уходит слишком много времени.
Кроме того, нам пришлось вручную формировать пакеты и структуры папок под три Source-набора: Android main, iOS main и Common main. Также было неудобно самим создавать build.gradle.kts и прописывать в них пути к исходникам.
Также были проблемы с общением между модулями. Студия подсвечивала все импорты и сущности из соседних модулей красным и не видела их, хотя все успешно собиралось и работало.
Если до этого момента вы успешно откладывали знакомство с Coroutines, здесь уже без них никак. С помощью Корутинов на Kotlin пишут асинхронный, неблокирующий код. У нас с ними все было ОК. Но коллеги рассказывали, что иногда Coroutines могут зависнуть в iOS по непонятным причинам, и это сложно предотвратить и контролировать. Насколько я знаю из новейших источников, этот момент пофиксили. Но есть другая проблема — iOS код может выдавать ошибки, которые превращаются в базовые iOS-ные.
Сейчас почти все проблемы ушли. Вы можете использовать последнюю версию android studio, и все будет прекрасно работать. Также Kotlin Multiplatform Mobile Plugin существенно упрощает создание новых мультиплатформенных проектов и поддержку текущих и даже позволяет дебажить код, собранный под IOS.
Подводя итоги, хотелось бы сказать, что все Android-разработчики давно привыкли использовать Kotlin. Круто, что совсем немного изменив нашу структуру модулей, мы можем получить код, который будет переиспользован. Так что давайте пробовать, заводить тикеты о проблемах, с которыми сталкиваемся, поднимать комьюнити и учиться на ошибках друг друга :)
8 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівНужно ли UI часть для IOS писать на Swift только, или можно использовать Flutter?
Можно на Flutter, но тогда уже логичнее писать на Kotlin общую логику, а UI для обоих платформ на Flutter.
Тут в статье, хотелось сделать акцент на том, что небольшое изменение привычек позволит в случае необходимости легко перейти на KMM, но в целом согласен с вами)
зачем писать на дарте, если он ничем не лучше а иногда даже хуже?
Этот паттерн мало того, что ведёт к необходимости писать кучу кода для описания интерфейса вьюх, который у вас как раз и не получится переиспользовать, так ещё и больно бьёт в кейсах поворота экрана.
Презентейшн лейер — это по определению платформо-зависимый модуль.
По моему мнению, хорошим решением было бы описать view states в виде Котлин классов и просто передавать их во viewModels.
Это позволило бы использовать MVVM, убрать все эти контракты и сбиндить вьюху с презентейшн напрямую. А viewState передавать из кроссплатформенного кода.
Добрый вечер, да согласен mvvm приятнее в сохранении состояний)
Я считаю что презентейшн — является платформозависимым если он отвечает за:
1. Рендеринг вьюх
2. Презентационную логику
В нашем подходе мы хотели минимизировать платформозависимый код поэтому разделили эти ответственности на два модуля, в UI модуле у нас остался только рендеринг то-есть реализации View контрактов из Presentation (немного не понял почему контракты не сможем переиспользовать)