Всем привет! Меня зовут Амет Хырхара, я Android разработчик в компании Joy Dev.
Первое, в чём может возникнуть затруднение у Android разработчика при переходе на КММ - это быстрая настройка окружения. Официальный сайт Kotlin не пестрит подробными инструкциями, и можно потратить несколько часов в поисках нужной информации. В данной статье мы пройдёмся по этапам настройки проекта.
Что такое КММ
КММ, или Kotlin Multiplatform Mobile, это технология кроссплатформенной разработки, которая даёт возможность объединить общую бизнес-логику приложения (к примеру, запросы на сервер, обработка данных, работа с БД) в отдельный модуль и при этом использовать нативный UI.
Настройка на Windows
Для того, чтобы создать КММ-проект, нам понадобится зайти в Android Studio, пройти по пути: File -> Settings -> Plugins, выбрать Marketplace и ввести в поисковую строку название инструмента. После этого установить плагин.
Когда плагин установлен, при создании проекта у вас появятся следующие опции. Выбираем первый вариант.
При создании в параметре ios framework distribution выбираем Regular Framework.
На этом всё, можно начинать писать. Простота установки окупается невозможностью полноценно писать КММ проекты.
MacOS
Чтобы в полную силу работать с КММ: редактировать специфический для iOS-платформ код и в последующем проводить отладку – необходимо устройство от Apple.
Для начала нам необходимо установить Homebrew - это package manager, который позволяет устанавливать недостающие пакеты и библиотеки на macOS и Linux. Открываем официальный сайт и, согласно инструкции, копируем команду, далее вставляем её в терминал - аналог командной строки.
После установки вводим следующую команду: brew install kdoctor
- инструмент командной строки, который проверяет вашу систему на наличие нужных файлов и их корректные версии для работы с КММ.
После установки вводим команду kdoctor
и ждём результат. Ответ должен быть примерно такой:
То есть, инструмент проверяет:
1) Систему. Если она не подходит, то, увы, не судьба.
2) Наличие JDK.
3) Наличие Android Studio и плагина КММ (устанавливаем, как в Windows)
4) Наличие xCode.
5) Наличие Cocoapods и его совместимость с Ruby.
Проблемы, связанные с первыми четырьмя пунктами, описаны здесь. На моём опыте проблемы возникли с Cocoapods и его совместимостью с Ruby, так как это не освещено в официальной документации. Но, прежде чем начать, давайте разберём, что такое cocoapods, Ruby, а также rvm и rbenv.
Ruby – язык программирования, на котором написаны некоторые библиотеки, используемые в проекте.
Cocoapods – менеджер зависимостей (dependency manager), который, в частности, написан на Ruby. Там прописываются наши зависимости (примерно, как в gradle). Список зависимостей обычно указывается в Podfile.
rvm и rbenv – инструменты, которые позволяют нам управлять версиями Ruby, обновлять или делать откаты.
После того, как kdoctor проанализировал систему, он подскажет, какую версию Ruby нам нужно установить (подсказки начинаются с *).
Чтобы установить Ruby нужной версии, пишем в терминал brew install ruby@2.7
Для установки cocoapods используйте команды sudo gem install cocoapods
и sudo gem install cocoapods-generate
Если ваша версия Kotlin меньше, чем 1.7.0, то Cocoapods-generate не установятся на Ruby версии 3.0.0 и выше.
Одна из возможных ошибок, которая может возникнуть на данном этапе, это совместимость версий, потому что КММ стабильно работает на версиях ниже текущих, а также версия Ruby, установленная по умолчанию, может отличаться от нужной нам (проверить, какая версия Ruby используется у нас, можно с помощью команды ruby -v).
Установка искомой версии Ruby. Здесь у нас есть два пути: rbenv и rvm.
rbenv
1) Устанавливаем с помощью Homebrew brew install rbenv ruby-build
2) Используем rbenv install –list
, чтобы увидеть список доступных версий, выбираем нужную
3) Устанавливаем версию rbenv install <версия>
4) Ставим её по умолчанию rbenv global <версия>
rvm
1) Установка \curl -sSL https://get.rvm.io | bash -s stable
2) Список версий rvm list known
3) Установка конкретной версии rvm install <версия>
4) Установка версии по умолчанию rvm use <версия> --default
После очередной работы kdoctor видим долгожданную надпись: Your system is ready for Kotlin Multiplatform Mobile Development! Можем создавать проект.
Использование Kotlin Multiplatform Wizard
Существует другой способ создания КММ-проекта: использовать Kotlin Multiplatform Wizard. Преимуществом будет выступать возможность автоматического подключения базовых библиотек, плагинов и выбор платформ, с которыми вы хотите работать (при обычном создании появляются только androidApp и iosApp). Затем скачать архив и открыть проект.
Обзор созданного проекта
После загрузки всех компонентов рекомендуем переключиться с вида Android на Project, чтобы видеть всю файловую систему проекта.
Модули и их предназначение
На этом этапе уже видно, что проект отличается от привычного. Вместо одного app модуля у нас создались три: androidApp, iosApp и shared. Если мы откроем shared/src, то увидим ещё три модуля – androidMain, commonMain, iosMain.
Давайте разбираться.
androidApp – здесь мы прописываем наши UI элементы (Android View или Jetpack Compose) и логику, которые присущи исключительно Android и не имеют альтернативы на других платформах. Например, Activity или Broadcast Receiver – в iOS нет таких сущностей в подобном виде, поэтому они могут использоваться только в androidApp модуле. Также в директории мы видим build.gradle.kts, где будем указывать зависимости исключительно для Android (фрагменты, навигация и т.п.), и тут же подключается shared модуль, который мы рассмотрим далее: implementation(project(":shared")).
iosApp – аналогия androidApp для iOS. Для того, чтобы редактировать код в этом модуле, понадобится xCode. Чтобы открыть проект на xCode, запускаем среду разработки, кликаем Open a project or file, в директории вашего проекта заходим в iosApp и открываем файл iosApp.xcodeproj.
Если вы изначально создавали проект на Windows, а затем перешли на macOS, у вас скорее всего появится ошибка gradlew Permission denied. Для её решения в терминале Android Studio следует ввести следующую команду chmod +x gradlew
shared – здесь мы описываем бизнес-логику приложения: от запросов на сервер до вью моделей. Основная часть кода будет расположена в commomMain.
Два других модуля нужны для написания логики, которая присуща обеим платформам, но немного отличается реализацией.
Например, в Android при использовании вью моделей мы наследуемся от класса ViewModel(), но в iOS этого делать не нужно. Поэтому в commomMain создаём директорию presentation, а в ней - абстрактный класс CommonViewModel. Добавляем ключевое слово expect (перед abstract), которое означает, что ожидается имплементация этого класса на каждой из платформ, в данном случае – androidMain и iosMain.
commonMain:
expect abstract class CommonViewModel()
В этих модулях мы тоже создаем папку presentation и абстрактный класс CommonViewModel, но expect заменяем на actual, означающее, что данный класс будет имплементирован. Перед конструктором также придётся прописать ключевое слово actual constructor. В iosMain мы оставим так, а в Android пронаследуемся от ViewModel().
iosMain:
expect abstract class CommonViewModel() actual constructor()
androidMain:
actual abstract class CommonViewModel actual constructor() : ViewModel()
Должно получиться как-то так:
Теперь при создании вью модели мы будем наследоваться от CommonViewModel и дёргать реализацию в соответствии с платформой, на которой мы её используем.
Использование expect-actual механизма легче понять, если воспринимать его как интерфейсы. Expect - это, по сути, интерфейс, который мы «переопределяем» (actual) в зависимости от ситуации.
В shared модуле также есть свой build.gradle.kts, где мы будем указывать наши общие зависимости. Обращаем внимание на sourceSets – здесь мы видим перечень знакомых нам модулей и их копий с суффиксом Test.
Зависимости, размещённые в commonMain, будут относиться ко всем целевым платформам. В androidMain – к Android. В iosMain – к iosX64Main, iosArm64Main, iosSimulatorArm64Main (на это намекает метод dependsOn()). При подключении библиотек путаница не возникает, потому что на официальных порталах всегда детально указывается, как их следует подключать. Следующим этапом мы разберём удобную технологию управления зависимостями в КММ.
Управление зависимостями с помощью buildSrc
По мере разрастания проекта и увеличения числа подключаемых библиотек возникают трудности с мониторингом версий и дублированием. В таком случае хорошим решением будет поместить все имеющие зависимости в отдельный модуль. Таким модулем может выступить buildSrc – специальная библиотека, которая подключается к Gradle-проекту (само название также зарезервировано Gradle-ом) и при сборке проекта компилируется первой.
Чтобы создать buildSrc модуль, на уровне проекта добавляем папку с соответствующим названием. Внутри создаём build.gradle.kts файл, вставляем следующий код и жмём sync.
repositories {
mavenCentral()
}
plugins {
`kotlin-dsl`
}
После этого в buildSrc модуле создаём директорию src/main/kotlin – если синхронизация прошла успешно, она должна появиться в предложенных. Далее в папке kotlin создаем файл Dependencies.kt. Здесь мы и будем хранить наши зависимости.
В файле создаём три объекта: Plugins, Versions, Deps (внутри него для удобства можно отдельно создать объекты Android и Multiplatform) и вставляем следующий код:
В Plugins указываем базовые плагины:
object Plugins {
const val androidApp = "com.android.application"
const val android = "android"
const val multiplatform = "multiplatform"
const val androidLib = "com.android.library"
}
В Versions указываем версии SDK, подключаемых плагинов и библиотек:
object Versions {
// SDK
const val compileSdk = 32
const val targetSdk = 32
const val minSdk = 21
// Plugins
const val android_version = "7.3.1"
const val kotlin_version = "1.7.10"
const val compose_version = "1.2.1"
const val compose_activity_version = "1.5.1"
// Coroutines
const val coroutines_version = "1.6.4"
}
В Deps прописываем зависимости для наших библиотек, используя ранее введённые версии:
Gradle-плагины для Kotlin и Android:
const val kotlin_gradle_plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin_version}"
const val android_gradle_plugin = "com.android.tools.build:gradle:${Versions.android_version}"
Зависимости для Android:
object Android {
// Compose
const val compose_ui = "androidx.compose.ui:ui:${Versions.compose_version}"
const val compose_ui_tooling = "androidx.compose.ui:ui-tooling:${Versions.compose_version}"
const val compose_ui_tooling_preview = "androidx.compose.ui:ui-tooling-preview:${Versions.compose_version}"
const val compose_foundation = "androidx.compose.foundation:foundation:${Versions.compose_version}"
const val compose_material = "androidx.compose.material:material:${Versions.compose_version}"
const val compose_activity = "androidx.activity:activity-compose:${Versions.compose_activity_version}"
// Coroutines
const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines_version}"
}
Зависимости для КММ:
object Multiplatform {
// Coroutines
const val coroutines_core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines_version}"
}
Примечание: в статье используются зависимости, которые подключаются при создании проекта, кроме корутин, которые специально взяты для демонстрации их включения в shared модуль.
Следующий шаг - преобразовать build.gradle.kts
файлы нашего проекта. Открываем корневой файл и заменяем строки на наши переменные.
buildscript {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
classpath(Deps.android_gradle_plugin)
classpath(Deps.kotlin_gradle_plugin)
}
}
В build.gradle.kts (:shared) обновляем плагины:
plugins {
kotlin(Plugins.multiplatform)
id(Plugins.androidLib)
}
Примечание: ключевое слово kotlin используется для автоматического добавления префикса org.jetbrains.kotlin в название плагина. То есть, выражения id(org.jetbrains.kotlin.multiplatform)
и kotlin(multiplatform)
тождественны.
Зависимости:
sourceSets {
val commonMain by getting {
dependencies {
implementation(Deps.Multiplatform.coroutines_core)
}
}
...
}
Так как мы добавили библиотеку корутин в commonMain, мы можем её использовать как для Android, так и для iOS.
SDK:
android {
namespace = "com.example.newkmm"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
targetSdk = Versions.targetSdk
}
}
В build.gradle.kts (:android):
plugins {
id(Plugins.androidApp)
kotlin(Plugins.android)
}
android {
namespace = "com.example.newkmm.android"
compileSdk = Versions.compileSdk
defaultConfig {
applicationId = "com.example.newkmm.android"
minSdk = Versions.minSdk
targetSdk = Versions.targetSdk
versionCode = 1
versionName = "1.0"
}
...
}
dependencies {
implementation(project(":shared"))
implementation(Deps.Android.compose_ui)
implementation(Deps.Android.compose_ui_tooling)
implementation(Deps.Android.compose_ui_tooling_preview)
implementation(Deps.Android.compose_foundation)
implementation(Deps.Android.compose_material)
implementation(Deps.Android.compose_activity)
}
Синхронизируем проект. Во вкладке Build убеждаемся, что сначала выполняются таски, связанные с buildSrc, а затем таски основного проекта.
КММ изначально собирается дольше, чем обычный Android проект, поэтому открываем в gradle.properties и добавляем следующие строки:
org.gradle.caching=true // 1
org.gradle.parallel=true // 2
org.gradle.daemon=true // 3
1 – включаем кеширование.
2 – включаем параллельную сборку не взаимосвязанных задач, так как по умолчанию Gradle выполняет одну задачу за раз. Фича особенно полезна в КММ.
3 – Gradle Daemon по умолчанию включен, но желательно указывать его явно. Этот компонент занимается кешированием, мониторингом файловой системы для определения необходимых для билда файлов и держит JVM в «прогретом» состоянии.
После этого проект будет собираться значительно быстрее.
Итог
Подготовка системы к кроссплатформенной разработке, которая от пользователей Windows требует пары минут, может занять продолжительное время для владельцев macOS, поэтому мы постарались подробно описать, где и какие инструменты можно использовать для достижения цели, а также устранили возможную путаницу с несовместимостью версий.
Проект на старте имеет более сложную файловую систему и изначально состоит из большего числа модулей. В shared модуле мы описываем общую логику, которую имеет приложение и по случаю прибегаем к expect/actual механизму (который легко воспринимать в качестве интерфейса), когда сущности платформы частично отличаются реализацией.
Для централизованного управления зависимостями можно использовать Gradle библиотеку buildSrc, которая позволяет хранить все зависимости и версии в одном файле, разбив их на группы, что также улучшает читабельность кода. При сборке этот модуль компилируется первым, поэтому проблем на этом этапе не возникает. Существует также альтернатива в виде композитной сборки, использовать её следует по ситуации. Об этом есть статья.
Теперь, когда самое скучное позади, можем приступать к написанию логики самого проекта.