====== Gradle ====== https://www.youtube.com/results?search_query=gradle+kotlin Gradle Tutorial - Crash Course: https://www.youtube.com/watch?v=gKPMKRnnbXU\\ Understanding Gradle (playlist): https://www.youtube.com/playlist?list=PLWQK2ZdV4Yl2k2OmC_gsjDpdIBTN0qqkE\\ Gradle и система сборки Android: https://www.youtube.com/watch?v=5SYaITerrjU Система сборки ПО с автоматизацией. https://docs.gradle.org/current/userguide/userguide.html ====== Терминология, строение ====== > Project - то, что собственно собирается. Скрипт сборки - ''build.gradle'', там декларируются Tasks, Plugins, Dependencies. >> Task - этап сборки, например, компиляция, тест, деплой. >>> Plugin - типа функции. Позволяет использовать одни и те же Tasks для разных целей, избавляет от дублирования кода. >>>> Build - сборка, выполнение набора задач в проекте. >>>>> Build phase - этап сборки (инициализация/конфигурация/выполнение) * ''build.gradle'' - основной файл. В подкаталогах (подпроектах/модулях) тоже может присутствовать. Регистрирует набор плагинов, используемых в проекте. Расширение файла ''build.gradle.kts'' указывает на то, что там используется Kotlin DSL вместо Groovy DSL, используемый раньше. plugins { id("com.android.application") version "8.0.1" apply false id("com.android.library") version "8.0.1" apply false id("org.jetbrains.kotlin.android") version "1.7.20" apply false } * ''gradle.properties'' - настройки сборочной логики проекта и плагинов. * ''local.properties'' - характерен для Андроид-проектов, в основном только указывает путь к SDK. * ''settings.gradle'' - какие модули/подпроекты включить в сборку. ... rootProject.name = "My app" include(":app") include(":feature1") ===== Android Gradle plugin (AGP) ===== plugins { id("com.android.application") } android { compileSdk = 33 // Обязательный блок, указываются версии самого ПО и SDK defaultConfig { applicationId = "com.example.myapplication" minSdk = 24 targetSdk = 33 versionCode = 1 versionName = "1.0" } signingConfigs { ... } buildTypes { release { ... } debug { ... } } productFlavors { ... } compileOptions { ... } splits { ... } } ==== buildTypes ==== Варианты сборки одного и того же пакета, например, release и debug. Здесь самый простой пример (''isMinifyEnabled'' - обфусцировать и выкидывать всё лишнее для релиза), но можно даже по-разному конфигурировать код для разных типов сборки. buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } debug { isMinifyEnabled = true signingConfig = signingConfigs.getByName("debug") } } ==== productFlavors ==== Похоже на buildTypes в том плане, что делаются разные пакеты, например, платный вариант приложения и бесплатный. flavorDimensions += "plan" productFlavors { create("paid") { dimension = "plan" buildConfigField("int", "TYPE", "\"PAID\"" } create("free") { dimension = "plan" buildConfigField("int", "TYPE", "\"FREE\"" } } ==== buildVariant ==== Совокупность buildTypes и productFlavors. Т. е., в рамках buildVariant собираются PaidDebug, PaidRelease, FreeDebug, FreeRelease и прочие, если он будут добавлены. ====== Зависимости ====== Зависимости - это - Сторонний код (библиотека), подключаемый к коду проекта - Сторонний код (библиотека), подключаемый к коду сборки проекта (т. к. Gradle основан на JVM и библиотеки можно подключать в сам Gradle) Gradle не просто подключает какую-то библиотеку, как это делается в проекте. Он скачивает jar/aar-файл, распаковывает его и линкует к проекту. Далее можно использовать этот код уже непосредственно в проекте. Чтобы подключить библиотеку, нужно указать - Откуда её скачать // попытки скачать с указанных ресурсов идут построчно, если ниоткуда скачать не получается - выдаётся ошибка repositories { google() mavenCentral() mavelLocal() maven("https://plugins.gradle.org/m2/") maven("https://own.repo.ru/") { credentials { username = System.getenv("REPO_USERNAME") password = System.getenv("REPO_PASSWORD") } } } - Как её скачать dependencies { // Указатель пакета - "namespace:package_name:version" implementation("androidx.core:core-ktx:1.8.0") api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") compileOnly("aaa:bbb:1.0.0") runtimeOnly("ccc:ddd:1.0.0") testImplementation("eee:fff:1.0.0") debugApi("ggg:hhh:1.0.0") implementation(project(":feature1")) } Важно различие api и implementation. Если имеются транзитивные зависимости, например, app -> zoo -> animals, то недостаточно будет указать ''implementation zoo'', т. к. animals тогда не подключится. Нужно либо подключать через implementation все зависимости, либо подключать animals к zoo по api; тогда app при ''implementation zoo'' увидит и animals. ===== Различия при подключении библиотеки к самому проекту или к сборке проекта ===== К проекту repositories { google() mavenCentral() } dependencies { implementation("androidx.core:core-ktx:1.8.0") } К сборке проекта: обернуть всё в ''buildscript {}'' и в dependencies указывать ''classpath()'' buildscript { repositories { google() mavenCentral() } dependencies { classpath("androidx.core:core-ktx:1.8.0") } } ===== Проблемы при подключении зависимостей ===== Самые распространённые - это - java.lang.NoSuchMethodError или java.langNoClassDefFoundError - Duplicate class У первого случая суть в том, что когда подключаются два одинаковых API разных версий, то Gradle выбирает самый новый по версии, а там, например, переименовали метод из ''getAccount()'' в ''fetchAccount()'', соответственно, при использовании ''getAccount()'' в firebase выскочит ошибка NoSuchMethodError. {{:service:pasted:20240408-081711.png?600}} Костыль для обхода этой проблемы - форсирование использования более старого API. configurations.all { resolutionStrategy { force("googleApi:1.0.0") } } Но тогда может возникнуть другая проблема, если библиотека googleCloud откажется работать со старой версией googleApi. Тут надо смотреть по ситуации. Второй случай (Duplicate class) возникает, если две библиотеки используют один и тот же класс. {{:service:pasted:20240408-083010.png?600}} Костыль для обхода - исключить (exclude) модуль googleApi, который тянет за собой firebase. dependencies { implementation("firebase") { exclude(group = "com.google", module = "googleApi") } implementation("googleCustomApi") } Опять же, это чревато тем, что firebase может работать неправильно или не работать вообще, либо код может не собраться и т. п. Ещё один пример exclude - guava (улучшенная работа с коллекциями) тянет за собой довольно тяжёлый модуль findbugs, который в проде вряд ли пригодится. {{:service:pasted:20240408-084253.png?600}} Тем не менее, необходимо хорошо понимать, в каких случаях что исключается и исследовать последствия таких исключений во избежание проблем. ====== Многомодульность ====== Многомодульность - это разделение кода проекта на подпроекты (модули) и правильная организация связей между этими модулями. Плюсы: * Деление на независимые части делает код более понятным и стройным * Меньше конфликтов при командной разработке - каждый разрабатывает свою часть приложения. Это не всегда так гладко работает - например, помимо разных модулей есть ещё общий модуль core, и может возникнуть ситуация, когда об разработчика коммитят ещё и туда, что может потенциально привести к конфликту. * Возможность сборки разных целей (targets) приложений на единой кодовой базе. К примеру, если есть облегчённая версия приложения, где набор модулей меньше, которую можно собирать отдельно, или разные приложения для разных пользователей (диспетчер и водитель), которые, тем не менее, основываются на той же самой кодовой базе. * В некоторых ситуациях, ускорение сборки (в ''gradle.properties'' надо включить ''org.gradle.parallel=true''). Не всегда всё может выполняться параллельно. Пока не соберётся модуль, от которого зависят другие, например, core, то сборка зависимых модулей не начнётся. Модуль app вообще собирается последним, т. к. он зависит от всех модулей, входящих в состав приложения. Минусы * Более сложная структура, её сложнее понять и больше времени нужно на освоение новым участником команды * Необходимость следить за связями между модулями для исключения конфликтов * В некоторых ситуациях, замедление сборки ===== Настройка многомодульности ===== После добавления каталогов (в них содержится код подпроектов/модулей) в корневую структуру проекта, нужно: Добавить их в файл ''settings.gradle.kts'', чтобы добавить модули в сборку include(":app") include(":core") include(":feature1") include(":feature2") include(":network") В модулях feature1 и feature2 (внутри их каталогов) нужно подключить другие модули pluigns { id("com.android.library") } dependencies { implementation(project(":core")) implementation(project(":network")) } В app прописывается всё, это центральная точка. pluigns { id("com.android.application") } dependencies { implementation(project(":core")) implementation(project(":network")) implementation(project(":feature1")) implementation(project(":feature2")) } ===== Рекомендации ===== Необходимо определить, для какой цели код проекта разбивается на модули. Если это какой-то маленький проект и полтора человека, смысла нет. В противном случае, конечно, разбивать надо. Нужно избегать прямой связи между feature-модулями. К примеру, есть core, от него зависят feature1 и feature2, а в конце идёт app. При ПЕРЕсборке проекта, если изменяется feature2, то пересобираться будут только feature2 и app (если изменяется core, то пересобирается всё).\\ Если есть зависимость между feature1 и feature2, то параллельности их сборки добиться не получится - всё будет выполняться последовательно из-за зависимостей одного от другого.\\ Лучше, чтобы от часто изменяемых модулей зависело как можно меньше других модулей. По той же причине, нужно делать поменьше общих core-модулей. ===== Побочные эффекты многомодульности ===== ==== Проблема дублирования зависимостей ==== К примеру, наблюдается такая картина в ''build.gradle.kts'': // app dependencies { implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.appcompat:appcompat:1.6.1") } // feature1 dependencies { implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.appcompat:appcompat:1.6.1") } // feature2 dependencies { implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.appcompat:appcompat:1.6.1") } //core dependencies { implementation("androidx.core:core-ktx:1.8.0") } В этом случае, чтобы обновить appcompat, нужно менять его версию в куче конфигов. Варианты решения этой проблемы: * Шарить версии библиотек через ext или gradle.properties * Version catalog (рекомендуется) === Version catalog === - Создать .toml с описанием зависимостей, к примеру, создать файл ''libs.toml'' в корне проекта - Задекларировать Version catalog dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("libs.toml")) } } } - Использовать Version catalog [versions] kotlin = "1.8.0" appcompat = "1.6.1" [libraries] kotlin-android-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "kotlin" } android-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } [bundles] feature-deps = ["kotlin-android-ktx","android-appcompat"] Теперь можно писать в ''build.gradle.kts'' // можно так, если в libs.toml нет секции [bundles] dependencies { implementation(libs.files.io) implementation(libs.lifecycle.runtime.ktx) } // или, если секция [bundles] есть, сразу так dependencies { implementation(libs.bundles.feature) } **Та же схема работает и для плагинов** - их так же можно подцепить через файл .toml. [versions] kotlin = "1.8.0" agp = "8.0.1" [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } android-library = { id = "com.android.library", version.ref = "agp" } plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.android.library) } Помимо удобства, описанного выше, Gradle умеет публиковать этот .toml как зависимость между проектами, например, внутри одной компании, т. е., другой проект может подключить его и использовать как список с версиями зависимостей. ==== Проблема дублирования кода конфигурации сборок ==== т. е., ''build.gradle'' в feature1 и feature2 выглядят практически идентично plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) } android { namespace = "com.example.feature1" compileSdk = 33 defaultConfig { minsdk = 24 } } dependencies { implementation (libs.bundles.feature.deps) } решение - convention plugins ==== Проблема увеличения времени конфигурации проекта ==== Решение - ''org.gradle.configuration-cache=true'', см. [[https://docs.gradle.org/current/userguide/configuration_cache.html|документацию по configuration cache]]) ===== Проблемы больших проектов ===== ==== Конфигурация сборки и доп. логика сборки начинает смешиваться, усложняя поддержку ==== m( С 49-й минуты - идиотское программистское красноглазие - мегаудобство, нечего сказать. Файлик ссылается на файлик, а тот ещё на файлик, а тот ещё на файлик. Если это удобство, тогда какой же ужас без этого Гредла со сборкой ява-приложений? Например, когда в ''build.gradle'' прописывается выгрузка файла в Google и т. п. Если ''build.gradle'' разрастается на несколько сотен строк, это уже тяжело читать и поддерживать. import java.io.File import java.net.URL tasks.register("publishToGoogle") { doLast { in uploadApkToGoogle(file(File(rootDir, "build/outputs/apk/release/release.apk")).readBytes()) } } tasks.findByName("publishToGoogle").dependsOn(tasks.findByName("assembleRelease")) Для решения этой проблемы изобрели Convention plugins. Создаётся каталог с любым именем (ну, т. е., ещё один плагин/подпроект), к примеру, ''build-logic''.\\ Затем, в корневом ''settings.gradle.kts'' добавляется строка в раздел ''pluginManagement {}''. pluginManagement { includeBuild("build-logic") } В ''build-logic'' создаётся свой файл ''settings.gradle.kts'' dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../libs.toml")) } } repositories { google() mavenCentral() } } Там же ''build.gradle.kts'' plugins { // без этого не взлетит "kotlin-dsl" } dependencies { implementation(libs.agp) implementation(libs.cotlin.gradle.plugin) // Костыль для работы version plugins внутри Convention plugins, иначе не работает (система ниппель!) implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } После этого можно создавать сами файлы Convention-плагинов. Вид дерева каталогов: {{:service:pasted:20240408-134709.png}} Названия файлов важны, т. к. к ним потом нужно будет обращаться. plugins { id("com.android.library") id("org.jetbrains.kotlin.android") } android { compileSdk = 33 defaultConfig { minSdk = 24 } } dependencies { implementation(libs.bundles.feature.deps) } В файле выше не будет работать конструкция ''android {}'' и ''implementation(libs.)'', т. к. этот код не генерируется в Convention plugins.\\ Поэтому надо найти плагин, где это не подсвечивается красным цветом, **перейти в исходники и скопировать исходники оттуда в код плагина вместо неработающих конструкций.** m( В файл ''publish-google-convention.gradle.kts'' выносится весь код закачки на Гугл, ранее бывший в ''app/build.gradle.kts'' import java.io.File import java.net.URL tasks.register("publishToGoogle") { doLast { in uploadApkToGoogle(file(File(rootDir, "build/outputs/apk/release/release.apk")).readBytes()) } } tasks.findByName("publishToGoogle").dependsOn(tasks.findByName("assembleRelease")) Теперь ''build.gradle.kts'' выглядят так: plugins { id("android-feature-convention") } android { namespace = "com.example.feature1" } plugins { id("publish-google-convention") } ===== Gradle wrapper ===== Рекомендуемый способ от разработчика Gradle для работы с ним. Структура файлов: {{:service:pasted:20240409-062254.png?600}} Здесь команда ''gradle build'' не сработает, нужно запускать скрипт ''./gradlew build'' или в винде ''gradlew.bat''. Основной параметр в файле ''gradle-wrapper.properties'' - это // Указывает, откуда враппер может скачать ту или иную версию Гредла, с которой он будет работать distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip Если Гредл уже скачан, то при вызове ''./gradlew build'' команда сборки сразу передаётся этой конкретной версии Гредла. Преимущества использования wrapper: * Легко использовать Гредл без предварительной установки * Стандартизация сборки. К примеру, при отправке на CI будет собираться именно с Гредлом, который указан в параметрах, а не с тем, который установлен на удалённой машине. * Быстрое переключение версии Гредла для проекта. ===== Gradle daemon ===== Это фоновый JVM-процесс, помогающий сокращать время сборки и конфигурации проекта (до 75% при повторной сборке). Что он делает: - Кэширует информацию о проектах между сборками - Висит в фоне, поэтому при запуске сборки не тратится время на инициализацию JVM - Следит за изменениями файлов проекта Схема работы: ''./gradlew build'' запускает Gradle client, а тот, в свою очередь, Gradle daemon. Если демона ещё нет, то он создаётся. Демон выполняет сборку и передаёт результат и логи клиенту, всё это отображается в терминале и клиент завершает работу вместе с запросом на сборку. Демон же продолжает висеть в фоне в статусе Idle и ждёт следующего обращения к нему. # Статус запущенных демонов ./gradlew --status # Запуск сборки с демоном из терминала ./gradlew build --daemon # Запуск сборки без демона ./gradlew build --no-daemon # Принудительно остановить все демоны (например, если не подхватываются переменные или ещё по каким-то причинам) ./gradlew --stop // Настройка Гредла для запуска сборки с демоном (false - без демона) org.gradle.daemon=true ===== Жизненный цикл сборки / Tasks ===== * Инициализация * Анализ ''settings.gradle.kts'', определение, какие проекты включены (include) в сборку * Создание экземпляра класса Project для каждого проекта/модуля * Конфигурация * Анализ сценария сборки из ''build.gradle.kts'' каждого проекта * Создание направленного ациклического графа задач (DAG) для каждого проекта. Задачи выполняются в порядке их зависимости друг от друга. * Выполнение * Планирование и выполнение всех задач в соответствии с построенным графом Gradle task - кусок логики, который выполняется при сборке проекта. Задачи могут выполнять разные действия: сборка apk-файла, заупск тестов, установка приложения на устройство, выгрузка apk в Маркет и т. д. Задача состоит из действий (actions), выполняющихся последовательно.