В сети хватает базовых примеров приложений на KMM, поэтому мы рассмотрим что-то, более приближенное к нашим ежедневным задачам разработчика, а именно, как реализовать многопоточное приложение на Kotlin Multiplatform.
Основная идея KMM, как и других кросс-платформенных технологий — оптимизация разработки путем написания кода один раз и последующего его использования на разных платформах.
Согласно концепции JetBrains, Kotlin Multiplatform не является фреймворком. Это именно SDK, который позволяет создавать модули с общим кодом, подключаемые к нативным приложениям.
Для взаимодействия с платформами используются специфические для платформы версии Kotlin: Kotlin/JVM, Kotlin/JS, Kotlin/Native. Данные версии включают расширения языка Kotlin, а также специфичные для конкретной платформы библиотеки и инструменты. Написанный на Kotlin модуль компилируется в JVM байткод для Android и LLVM байткод для iOS.
Модуль (Shared, Common) содержит переиспользуемую бизнес-логику. Платформенные модули iOS/Android, к которым подключен Shared/Common, либо используют написанную логику напрямую, либо имплементируют свою реализацию в зависимости от особенностей платформы.
Общая бизнес-логика может включать в себя:
- сервисы для работы с сетью;
- сервисы для работы с БД;
- модели данных.
Также в нее могут входить архитектурные компоненты приложения, напрямую не включающие UI, но с ним взаимодействующие:
- ViewModel;
- Presenter;
- Интеракторы и т.п.
Концепцию Kotlin Multiplatform можно сравнить с реализацией Xamarin Native. Однако, здесь нет модулей или функционала, реализующих UI. Эта логическая нагрузка ложится на подключенные нативные проекты.
Теперь рассмотрим подход на практике.
Если вы еще не работали с KMM, то потребуется установить и настроить инструменты. Раньше это было довольно хлопотно, но сейчас достаточно установить Android Studio (версии от 4.1) и плагин Kotlin Multiplatform Mobile . Выбираем шаблон KMM Application при создании проекта, и все отработает автоматически.
Мультиплатформенные проекты Kotlin обычно делятся на несколько модулей:
- модуль переиспользуемой бизнес-логики (Shared, commonMain и т.п);
- модуль для IOS приложения (iOSMain, iOSTest);
- модуль для Android приложения (androidMain, androidTest).
В них располагается наша бизнес-логика. Всю используемую в проекте бизнес-логику можно разделить на:
- переиспользуемую (общую);
- платформенную реализацию.
Переиспользуемая логика располагается в проекте commonMain в каталоге kotlin и разделяется на package. Декларации функций, классов и объектов, обязательных к переопределению, помечаются модификатором expect:
Реализации должны иметь модификатор actual.
В качестве примера работы с многопоточностью рассмотрим небольшое приложение, обращающееся к стороннему API по сети:
Я выбрала www.themoviedb.org. Полный код примера будет по ссылке внизу статьи.
В общей Common части расположим общую бизнес-логику:
А именно наш сетевой сервис. Это логично.
В модулях iOS/Android приложений оставим только UI компоненты для отображения списка и адаптеры. iOS часть будет написана на Swift, Android – на Kotlin.
Начнем с бизнес-логики. Т.к весь функционал будет в модуле common, то мы будем использовать в качестве библиотек решения для Kotlin Multiplatform:
Ktor – библиотека для работы с сетью и сериализации.
В build.gradle (:app) пропишем следующие зависимости:
Также добавим поддержку сериализации:
Далее нам надо определить, что делать с многопоточностью, ведь она реализуется по-разному на каждой платформе. На стороне iOS мы используем GCD (Grand Central Dispatch), а на стороне Android — JVM Threads и Coroutines. Однако, в Kotlin Multiplatform мы можем сделать общей и работу с многопоточностью.
Для этого мы будет использовать Kotlin Coroutines:
Тут стоит сделать пояснение, как с этим работать, потому что далеко не все iOS разработчики знают, что такое Coroutines. Если вкратце, то это блок кода, который можно приостановить, не блокируя поток. У корутины может быть контекст выполнения (CoroutineContext), цикл жизни корутины управляется Job. У корутины есть область действия (CoroutineScope), а поток, в котором она исполняется, задается с помощью CoroutineDispatcher.
Если проводить аналогию с iOS, то это похоже на выполнение блока кода в DispatchQueue, имеющей определенный QoS и привязку к определенному потоку NSThread, либо Operation в OperationQueue, где GlobalScope аналогичен DispatchQueue.global(), а MainScope — DispatchQueue.main:
Еще одной ключевой особенностью корутин является использование слова suspend. Данный модификатор не превращает метод в асинхронный сам по себе, это зависит от других деталей реализации, но маркирует, что его можно приостановить без блокировки потока. Также такой метод можно вызывать только в контексте корутины.
Ktor использует механизм корутины для реализации асинхронной работы, поэтому вызов HttpClient делаем в suspended функции:
При подключении Kotlin Coroutines мы не указали никакую особую версию для iOS. Это не ошибка. Дело в том, что начиная с версии Kotlin 1.4 Suspended функция Kotlin легко трансформируется в функцию Swift c completion handler блоком:
Т. к. Ktor уже обеспечивает асинхронность, то в данном случае потребности в использовании DispatchQueue на стороне iOS нет.
На стороне Android используем механизм корутинов, и вызов будет иметь вид:
Такой способ обращения к общей логике мы можем использовать при подходе, когда у нас нет общего архитектурного элемента наших нативных приложений, и в Common проекте мы реализуем только бизнес-логику. Это вполне рабочий подход.
Если же мы хотим сделать максимально расшариваемый между наивными проектами общий код, включить туда архитектурное решение, а взаимодействие с UI через протоколы, то нам потребуется поменять работу и с потоками.
Посмотрим это в следующей части, следите за обновлениями блога!
Исходники примера находятся здесь
┏━━━━ ▸▹◉◈◉◃◂ ━━━━┓
Зачем разработчикам изучать Kotlin? Это достаточно молодой язык, но уже набравший большую популярность. Он современный, лаконичный, безопасный и удобный для начинающих программистов.
20 декабря в 19:00 приглашаем на бесплатный вебинар «Начало работы с ООП в Kotlinc».
Изучим основы объектно-ориентированного программирования (ООП) в Kotlin. Для этого узнаем зачем вообще оно нужно в программировании и в чем преимущества использования классов и объектов. Вторым шагом мы разберем все возможные типы классов, научимся их создавать и узнаем области применения каждого зи типов. В завершение, разберем различные типы связывания классов: имплементация, наследование и делегирование.