Содержание

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 - этап сборки (инициализация/конфигурация/выполнение)

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 и прочие, если он будут добавлены.

Зависимости

Зависимости - это

  1. Сторонний код (библиотека), подключаемый к коду проекта
  2. Сторонний код (библиотека), подключаемый к коду сборки проекта (т. к. Gradle основан на JVM и библиотеки можно подключать в сам Gradle)

Gradle не просто подключает какую-то библиотеку, как это делается в проекте. Он скачивает jar/aar-файл, распаковывает его и линкует к проекту. Далее можно использовать этот код уже непосредственно в проекте.

Чтобы подключить библиотеку, нужно указать

  1. Откуда её скачать
    // попытки скачать с указанных ресурсов идут построчно, если ниоткуда скачать не получается - выдаётся ошибка
    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")
            }
        }
    }
  2. Как её скачать
    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")
    }
}

Проблемы при подключении зависимостей

Самые распространённые - это

  1. java.lang.NoSuchMethodError или java.langNoClassDefFoundError
  2. Duplicate class

У первого случая суть в том, что когда подключаются два одинаковых 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, который в проде вряд ли пригодится.

Тем не менее, необходимо хорошо понимать, в каких случаях что исключается и исследовать последствия таких исключений во избежание проблем.

Многомодульность

Многомодульность - это разделение кода проекта на подпроекты (модули) и правильная организация связей между этими модулями.

Плюсы:

Минусы

Настройка многомодульности

После добавления каталогов (в них содержится код подпроектов/модулей) в корневую структуру проекта, нужно:

Добавить их в файл 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, нужно менять его версию в куче конфигов.

Варианты решения этой проблемы:

Version catalog

  1. Создать .toml с описанием зависимостей, к примеру, создать файл libs.toml в корне проекта
  2. Задекларировать Version catalog
    settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("libs") {
                from(files("libs.toml"))
             }
        }
    }
  3. Использовать Version catalog
    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.

libs.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" }
feature1/build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.android.library)
}

Помимо удобства, описанного выше, Gradle умеет публиковать этот .toml как зависимость между проектами, например, внутри одной компании, т. е., другой проект может подключить его и использовать как список с версиями зависимостей.

Проблема дублирования кода конфигурации сборок

т. е., build.gradle в feature1 и feature2 выглядят практически идентично

build.gradle.kts
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)

Проблемы больших проектов

Конфигурация сборки и доп. логика сборки начинает смешиваться, усложняя поддержку

m( С 49-й минуты - идиотское программистское красноглазие - мегаудобство, нечего сказать. Файлик ссылается на файлик, а тот ещё на файлик, а тот ещё на файлик. Если это удобство, тогда какой же ужас без этого Гредла со сборкой ява-приложений?

Например, когда в build.gradle прописывается выгрузка файла в Google и т. п. Если build.gradle разрастается на несколько сотен строк, это уже тяжело читать и поддерживать.

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"))

Для решения этой проблемы изобрели Convention plugins.

Создаётся каталог с любым именем (ну, т. е., ещё один плагин/подпроект), к примеру, build-logic.
Затем, в корневом settings.gradle.kts добавляется строка в раздел pluginManagement {}.

pluginManagement {
  includeBuild("build-logic")
}

В build-logic создаётся свой файл settings.gradle.kts

build-logic/settings.gradle.kts
dependencyResolutionManagement {
  versionCatalogs {
    create("libs") {
      from(files("../libs.toml"))
    }
  }
  repositories {
    google()
    mavenCentral()
  }
}   

Там же build.gradle.kts

build-logic/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-плагинов. Вид дерева каталогов:

Названия файлов важны, т. к. к ним потом нужно будет обращаться.

android-feature-convention.gradle.kts
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

publish-google-convention.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 выглядят так:

feature1/build.gradle.kts
plugins {
  id("android-feature-convention")
}
 
android {
  namespace = "com.example.feature1"
}
app/build.gradle.kts
plugins {
  id("publish-google-convention")
}

Gradle wrapper

Рекомендуемый способ от разработчика Gradle для работы с ним. Структура файлов:

Здесь команда gradle build не сработает, нужно запускать скрипт ./gradlew build или в винде gradlew.bat.

Основной параметр в файле gradle-wrapper.properties - это

gradle-wrapper.properties
// Указывает, откуда враппер может скачать ту или иную версию Гредла, с которой он будет работать
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip

Если Гредл уже скачан, то при вызове ./gradlew build команда сборки сразу передаётся этой конкретной версии Гредла.

Преимущества использования wrapper:

Gradle daemon

Это фоновый JVM-процесс, помогающий сокращать время сборки и конфигурации проекта (до 75% при повторной сборке). Что он делает:

  1. Кэширует информацию о проектах между сборками
  2. Висит в фоне, поэтому при запуске сборки не тратится время на инициализацию JVM
  3. Следит за изменениями файлов проекта

Схема работы: ./gradlew build запускает Gradle client, а тот, в свою очередь, Gradle daemon. Если демона ещё нет, то он создаётся. Демон выполняет сборку и передаёт результат и логи клиенту, всё это отображается в терминале и клиент завершает работу вместе с запросом на сборку. Демон же продолжает висеть в фоне в статусе Idle и ждёт следующего обращения к нему.

# Статус запущенных демонов
./gradlew --status
# Запуск сборки с демоном из терминала
./gradlew build --daemon
# Запуск сборки без демона
./gradlew build --no-daemon
# Принудительно остановить все демоны (например, если не подхватываются переменные или ещё по каким-то причинам)
./gradlew --stop
gradle.properties
// Настройка Гредла для запуска сборки с демоном (false - без демона)
org.gradle.daemon=true

Жизненный цикл сборки / Tasks

Gradle task - кусок логики, который выполняется при сборке проекта. Задачи могут выполнять разные действия: сборка apk-файла, заупск тестов, установка приложения на устройство, выгрузка apk в Маркет и т. д. Задача состоит из действий (actions), выполняющихся последовательно.