My Authors
Read all threads
Сегодня среда и рассказывать я буду о Kotlin Multiplatform для мобильной разработки. Вначале будет немного истории нашего пути, что мы пробовали из других кроссплатформ, как дошли до мпп. Отдельным тредом накидаю всяких неочевидностей из нашего опыта
Так как мы занимаемся заказной разработкой - мы часто запускаем новые проекты и по новой делаем схожие фичи. Еще и на обеих платформах. Это не особо весело технарям и не особо круто бизнесу в деньгах. Всегда хотелось найти способ делать разом на обе платформы, без потери качества
В поисках решения пробовали мы React/Native (несколько проектов на нем были сделаны), но большой состав нативных мобильщиков не горел желанием переходить в JS и изучать React. Плюс словили несколько неприятных кейсов на разработке проектов с этой технологией.
Был кейс что мы разработали приложение, все работало, потом разработка некоторое время была в паузе, а когда решили продолжить работы оказалось что на текущих инструментах (Android Studio и Xcode) проект уже не собирается, надо обновлять R/N, а там breaking changes
Надеюсь на данный момент такого уже нет конечно.
Другой кейс, что мне запомнился, это поддержка восстановления состояния android приложения. а точнее ее отсутствие - клиент пожаловался что приложение сбрасывается на стартовый экран, мы пошли искать как сделать instanceState...
А в итоге нашли issue где ответом было - мы поддержку этого механизма не планируем, сохраняйте все в persistent storage. То есть пусть пользователь выключит вручную приложение - мы все равно его отправим именно на тот экран где он выключил апп. Игнор системной логики
Направление R/N в результате у себя свернули, вернулись к нативкам.
Но идея одного кода на обе платформы оставалась - в идеале хотелось чтобы вся бизнеслогика была общая, а UI - нативными инструментами.
Даже пробовал сделать с R/N так, но когда понял что любой вызов общей логики (которая в JS) будет асинхронный - прекратил эксперимент.
Попробовали еще Java 2 ObjC от гугла, даже попали на пару проектов где это уже было внедрено. На одном было все терпимо в плане использования..
А на другом я сам отлаживал iOS приложение исправляя утечки памяти, вызванные j2objc. Все таки писать Java реализацию нужно с учетом того, что код будет конвертирован в ObjectiveC. И пользоваться со стороны iOS полученным ObjC кодом не удобно - очень монструозный код.
Так же поэкспериментировали и с Flutter - этот фреймворк очень понравился скоростью разработки. Более быстрой и простой разработки еще не пробовал. Это обеспечивается за счет Hot Reload'а с полным сохранением состояния и хорошими инструментами отладки.
Но рисование своих подставных вьюх - не подходит нам в проектах, где дизайн не является полностью кастомным (там наоборот flutter рулит). Так же печалит поддержка системных фичей - поля ввода на iOS например не позволяют передвигать каретку, нет fill-password
На проекте который мы решили делать на Flutter (там универсальный для обеих платформ дизайн, весь кастомный) клиент, который пользуется iOS постоянно, заметил проблему со скроллом - при медленном скролле отрисовка лагала...в итоге нашли issue, которая плавает между версий
Основные причины не идти на Flutter были:
1. Абсолютно новый фреймворк и для iOS и для Android
2. Не нативный UI
3. Асинхронная коммуникация с платформенными фичами
4. Различные неожиданные баги (совсем не решающее значение)
В итоге поиски решения для общего кода привели нас к Kotlin Multiplatform, летом 2018 года. Тогда технология была очень сырой, еще даже ktor, coroutines, serialization не так то просто было завести и не все работало как надо
Но запилив небольшой пример с общей либой на kmp я понял что этот вариант общего кода то, что я хотел :)
Это привычный андроид команде kotlin, привычный им же gradle - половина мобильного направления будет в своем привычном окружении
Для iOS можно предоставлять готовый framework, который подключается как любой другой нативный framework и имеет настоящие классы и методы, а не каналы для асинхронной коммуникации с общим кодом.
Что именно расположить в общем коде можно регулировать по своему желанию - хочешь только сетевые сущности, хочешь всю сеть, хочешь всю бизнеслогику...упорешься - можно и вообще все на котлине, включая UI
Подход с expect/actual на мой взгляд очень аккуратный и наглядный - нет мешанины в коде с проверкой
if(android) ... else ...
есть просто классы/функции которые имеют общее API в common и по разному реализуются на платформах.
Похожее можно с интерфейсами, но экземпляр expect класса можно создать, без прокидывания каких либо фабрик, которые создают экземпляр объекта с нужным интерфейсом
Kotlin/Native предоставляет прямой доступ до всех нативных API. Так же как и Kotlin/Android. Интеграция с нативными API бесшовная, просто делается в платформенных sourceSet.
Обе платформы могут использовать в actual части свои платформенные бибилиотеки. Не только системные, но и внешние. В K/N реализована поддержка CocoaPods на уровне gradle, для упрощения интеграции. Но есть ограничение - в фреймворке должны быть ObjectiveC хидеры
Связано это с тем, что Kotlin/Native интеропится только с ObjectiveC. Поэтому и все iOS API со стороны Kotlin выглядят так, как они выглядят в ObjectiveC. Интеропа с Swift нет, но есть вероятность что будет добавлен.
Интероп только с ObjC не означает что в Swift коде нельзя увидеть то что сделано в Kotlin - Swift имеет свой интероп с ObjC, поэтому все прекрасно видно.
Но из-за ObjC накладывается несколько ограничений на API K/N библиотеки.
Например Generic со стороны Swift будет виден только для классов. А интерфейсы и функции Generic type потеряют, так как ObjectiveC такое не поддерживает в своем синтаксисе.
Так же в Kotlin на уровне языка есть suspend функции, чего нет ни в ObjC ни в Swift. Поэтому все такие функции сейчас просто стираются из хидера при компиляции фреймворка.
Но это исправится - будет автоматическое добавление аргумента с калбеком, а сейчас это делают плагины
Еще в Kotlin есть abstract class. Но в ObjC и Swift такого понятия нет. Да и понятия protected поля/метода тоже нет. И поэтому со стороны Swift мы видим Kotlin класс как обычный, а не абстрактный класс. а protected методы становятся публичными
Еще про Generic'и - даже если использовать их только для типизации классов, все равно можно получить Any тип со стороны Swift, если использовать in/out ограничения. Для получения удобного для iOS разработчиков API такие моменты должны учитываться
Есть vararg, которые нельзя преобразовать в variadic (objc аналог) аргументы. То есть сделать свой expect String.format который на iOS использует NSString(format: , args) не удастся. У нас для такого кейса сделан хак на 10 аргументов
bit.ly/39Gj7HX
Но с K/N можно сделать хитрые штуки в виде дополнений на ObjC для обхода кривого интеропа - прямо в def файле написать нужный метод, который скомпилится нативно как objc, а потом уже прокинется внутрь kotlin.
bit.ly/2IF9o8V
Впервые нам это потребовалось из-за хитростей iOS с обработкой plural локализаций - в NSString сохранялась дополнительная мета информация, которая становилась недоступна после преобразования в котлиновский String. Поэтому нужно было сделать получение локализованного plural в objc
Есть на данный момент у K/N проблема в interop'е с iOS - нельзя переопределить методы и свойства, которые в iOS SDK находятся в экстеншенах. В части кейсов это можно обойти используя аннотацию @ObjCAction и без override, но там где надо вызвать super - все плохо.
Однако это не является непреодолимым барьером. K/N позволяет подключить к нему свой Swift/ObjC фреймворк, в котором есть публичная objc API и вызывать его методы/классы из Kotlin. То есть уперевшись в ограничения можно унести код в Swift и не париться
Как раз гибкость K/N очень радует. При разработке мы сами вольны решать что будет в общем коде, что нативно. Что мы будем прокидывать внутрь котлина как зависимость, а что будет использовать котлин фреймворк снаружи. Такую гибкость не дает никакой другой инструмент общего кода
Отвлекся от истории :)
После небольшого теста сырой технологии я примерно месяц готовил проект-основу, чтобы на базе нее сделать какой либо реальный проект. Было много возни с mpp моделью в gradle, с coroutines, с serialization и ktor.
Пока не наткнулись на ktor вообще думал вынести на натив сеть и кидать через Retrofit/Alamofire. Пытался даже Alamofire внутрь Kotlin пробросить, но на стартовом этапе это совсем не удалось. Сериализация на тот момент не поддерживала в K/N аннотации - только JsonObject
В результате получился проект, на основе которого можно сделать нечто, где в общей логике будут кидаться запросы к серверу, делаться парсинг, выполняться бизнес-логика в каких нибудь viewmodel'ях. Дальше оставалось определить проект для эксперимента.
И такой проект нашелся, совсем небольшой - 3 экрана, пара запросов к серверу, без локального хранилища, только интеграция QR кода и NFC, которые на платформах независимо делаются и просто вызывают у ViewModel onCodeRead(code: String). Реализовывать проект согласился iOS разраб :)
Для него это был полностью новый опыт - и gradle и kotlin. Общую логику писал самостоятельно, после swift довольно быстро влился в kotlin, но код конечно сильно походил на swift, мышление iOS разраба было заметно. Все проблемы с gradle решали совместно.
За несколько недель приложение было готово (на iOS), общая логика была проверена. Дальше подклчился Android разработчик и за несколько дней добавил экраны на Android, подключил логику и все завелось.
Этот кейс очень вдохновил на дальнейшие инвестиции в технологию
Особенности mpp и неочевидные вещи из опыта продолжу накидывать в этот основной тред.
Есть неприятная особенность K/N стороны - инструменты отладки скудные. Мы имеем нативный фреймворк, можем ставить брейкпоинты даже в kotlin коде, можем видеть стектрейс понятный...
Но видеть что внутри переменных - в большинстве случаев нет. При чем нужно в отладчик xcode прицепить скрипт, который сможет смотреть внутрь котлин объектов.
bit.ly/2TQ4s6g
JetBrains работают над улучшением отладки, но сейчас часто приходится прибегать к println :)
А отладка с println подталкивает нас к другой актуальной проблеме - скорость сборки Kotlin/Native. Сборка производится в разы дольше чем для Android. И дольше чем Swift на iOS. Пока приоритеты JB не позволяли сильно работать над оптимизацией, хотя в 1.3.70 сделали прирост
Но в будущем очень надеюсь на реальное ускорение компилятора, побольше использования параллельных вычислений, каких нибудь хитростей для сокращения операций, добавление поддержки инкрементальной компиляции.
Однако эту проблему можно ослабить
Самое очевидное - наращивание мощностей. Посильнее процессор и побольше оперативной памяти. При чем оперативка может сыграть злую шутку, при дальнейших оптимизациях что я расскажу.
Со стороны проекта лучшее что можно сделать - разбить проект на множество gradle модулей.
Разделение по модулям должно быть продуманное, сделанное так, чтобы как можно реже изменение одного модуля требовало перекомпиляцию всех модулей. K/N не имеет инкрементальной компиляции - если что-то в модуле поменялось то он полностью компилится по новой
Если перекомпилилась какая-то зависимость модуля - то модуль тоже перекомпилится по новой. и так по цепочке.
Поэтому у нас есть разбиение на фичи, есть отдельно домен и между собой фичи и домен никак не связаны.
То есть двухуровневая структура - единый mpp-library, который компилируется в framework, а в него подключен десяток фичей, которые не имеют связи нискем из других модулей, только внешние зависимости. И домен с сетью и данными тоже подключен к mpp-library
В самом mpp-library настроена вся связь между фичами и доменом. В результате при изменении какой либо из фичей мы не перекомпилируем все фичи - только ее и mpp-library. Меняем домен - тоже самое. Этим экономится много времени. А clean build для ios может быть в 5-10 минут
Имея многомодульный проект, можно включить параллельную сборку на уровне gradle и получить как сильный прирост скорости, так и сильную деградацию. Зависит все от ресурсов. По умолчанию gradle паралелит на количество ядер процессора, но компилятор kotlin/native прожерлив...
И прожорлив компилятор в плане оперативки - если на вашем маке 8 гигов памяти, но процессор i7 с 12 логическими ядрами, то вы получите 12 параллельных сборок, которые сожрут всю память, весь swop и вместо ускорения сборки все будет замедленно в несколько раз.
В таком случае следует ограничить количество worker'ов у gradle под терпимое для вашего mac. Тогда и параллельная сборка будет, и ускорение, и память не сожрется.
Себе же я для максимальной эффективности взял macbook pro с 32г памяти и i9 процессором. Разница с macmini в 2-3 раза
А увидев разницу в скорости подключил свой мак как раннер для нашего gitlab CI, чтоб живее проходили сборки приложений, что железу простаивать :)
Кстати при многомодульном проекте точно возникнет вопрос - куда делись классы от подключенных модулей? где они в хидере?
И подключение модулей через api эту проблему не исправит. А все потому что надо для framework указать какие зависимости надо включить в хидер. через export
export (bit.ly/2IAxxxB) говорит K/N что надо все публичное API указанной зависимости включить в хидер фреймворка. Работает и для модулей проекта и для внешних зависимостей
Забыл про опросы, а хотел как раз статистику с такой аудитории собрать :)

Знакомы ли вы с Kotlin Multiplatform?
Не знал про такие жесткие ограничения твиттер-опросов...поэтому несколько еще сделаю, не влезло в один.
Для тех кто использует Multiplatform в продакшене, какие ваши впечатления?
Те кто не использует MPP - что останавливает?

p.s. варианты ответов не подразумевают наличие страшных проблем, мне хочется понять что думает аудитория :)
Как относятся ваши iOS разработчики к MPP?
Только для iOS разработчиков использующих MPP - ваши впечатления?
Для iOS разработчиков противников MPP - почему против?

Если другие причины - пожалуйста напишите в комменты, для меня этот вопрос один из самых важных на данный момент
Для всех мобильных разработчиков - как вам сама идея иметь общий код между Android и iOS? Не рассматриваем именно KMP - вопрос просто о самой идее часть кода в чистом виде реюзать между платформами.
Missing some Tweet in this thread? You can try to force a refresh.

Enjoying this thread?

Keep Current with Мобильный разработчик

Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

Twitter may remove this content at anytime, convert it as a PDF, save and print for later use!

Try unrolling a thread yourself!

how to unroll video

1) Follow Thread Reader App on Twitter so you can easily mention us!

2) Go to a Twitter thread (series of Tweets by the same owner) and mention us with a keyword "unroll" @threadreaderapp unroll

You can practice here first or read more on our help page!

Follow Us on Twitter!

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just three indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3.00/month or $30.00/year) and get exclusive features!

Become Premium

Too expensive? Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal Become our Patreon

Thank you for your support!