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")
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 { ... } }
Варианты сборки одного и того же пакета, например, release и debug. Здесь самый простой пример (isMinifyEnabled
- обфусцировать и выкидывать всё лишнее для релиза), но можно даже по-разному конфигурировать код для разных типов сборки.
buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } debug { isMinifyEnabled = true signingConfig = signingConfigs.getByName("debug") } }
Похоже на buildTypes в том плане, что делаются разные пакеты, например, платный вариант приложения и бесплатный.
flavorDimensions += "plan" productFlavors { create("paid") { dimension = "plan" buildConfigField("int", "TYPE", "\"PAID\"" } create("free") { dimension = "plan" buildConfigField("int", "TYPE", "\"FREE\"" } }
Совокупность buildTypes и productFlavors. Т. е., в рамках buildVariant собираются PaidDebug, PaidRelease, FreeDebug, FreeRelease и прочие, если он будут добавлены.
Зависимости - это
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") } }
Самые распространённые - это
У первого случая суть в том, что когда подключаются два одинаковых API разных версий, то Gradle выбирает самый новый по версии, а там, например, переименовали метод из getAccount()
в fetchAccount()
, соответственно, при использовании getAccount()
в firebase выскочит ошибка NoSuchMethodError.
Костыль для обхода этой проблемы - форсирование использования более старого API.
configurations.all { resolutionStrategy { force("googleApi:1.0.0") } }
Но тогда может возникнуть другая проблема, если библиотека googleCloud откажется работать со старой версией googleApi. Тут надо смотреть по ситуации.
Второй случай (Duplicate class) возникает, если две библиотеки используют один и тот же класс.
Костыль для обхода - исключить (exclude) модуль googleApi, который тянет за собой firebase.
dependencies { implementation("firebase") { exclude(group = "com.google", module = "googleApi") } implementation("googleCustomApi") }
Опять же, это чревато тем, что firebase может работать неправильно или не работать вообще, либо код может не собраться и т. п.
Ещё один пример exclude - guava (улучшенная работа с коллекциями) тянет за собой довольно тяжёлый модуль findbugs, который в проде вряд ли пригодится.
Тем не менее, необходимо хорошо понимать, в каких случаях что исключается и исследовать последствия таких исключений во избежание проблем.
Многомодульность - это разделение кода проекта на подпроекты (модули) и правильная организация связей между этими модулями.
Плюсы:
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, нужно менять его версию в куче конфигов.
Варианты решения этой проблемы:
libs.toml
в корне проектаdependencyResolutionManagement { versionCatalogs { create("libs") { from(files("libs.toml")) } } }
[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
, см. документацию по configuration cache)
С 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-плагинов. Вид дерева каталогов:
Названия файлов важны, т. к. к ним потом нужно будет обращаться.
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.
Поэтому надо найти плагин, где это не подсвечивается красным цветом, перейти в исходники и скопировать исходники оттуда в код плагина вместо неработающих конструкций.
В файл 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 для работы с ним. Структура файлов:
Здесь команда gradle build
не сработает, нужно запускать скрипт ./gradlew build
или в винде gradlew.bat
.
Основной параметр в файле gradle-wrapper.properties
- это
// Указывает, откуда враппер может скачать ту или иную версию Гредла, с которой он будет работать distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
Если Гредл уже скачан, то при вызове ./gradlew build
команда сборки сразу передаётся этой конкретной версии Гредла.
Преимущества использования wrapper:
Это фоновый JVM-процесс, помогающий сокращать время сборки и конфигурации проекта (до 75% при повторной сборке). Что он делает:
Схема работы: ./gradlew build
запускает Gradle client, а тот, в свою очередь, Gradle daemon. Если демона ещё нет, то он создаётся. Демон выполняет сборку и передаёт результат и логи клиенту, всё это отображается в терминале и клиент завершает работу вместе с запросом на сборку. Демон же продолжает висеть в фоне в статусе Idle и ждёт следующего обращения к нему.
# Статус запущенных демонов ./gradlew --status # Запуск сборки с демоном из терминала ./gradlew build --daemon # Запуск сборки без демона ./gradlew build --no-daemon # Принудительно остановить все демоны (например, если не подхватываются переменные или ещё по каким-то причинам) ./gradlew --stop
// Настройка Гредла для запуска сборки с демоном (false - без демона) org.gradle.daemon=true
settings.gradle.kts
, определение, какие проекты включены (include) в сборкуbuild.gradle.kts
каждого проектаGradle task - кусок логики, который выполняется при сборке проекта. Задачи могут выполнять разные действия: сборка apk-файла, заупск тестов, установка приложения на устройство, выгрузка apk в Маркет и т. д. Задача состоит из действий (actions), выполняющихся последовательно.