Инструменты пользователя

Инструменты сайта


service:gradle

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

Зависимости

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

  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, который в проде вряд ли пригодится.

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

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

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

Плюсы:

  • Деление на независимые части делает код более понятным и стройным
  • Меньше конфликтов при командной разработке - каждый разрабатывает свою часть приложения. Это не всегда так гладко работает - например, помимо разных модулей есть ещё общий модуль 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

  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:

  • Легко использовать Гредл без предварительной установки
  • Стандартизация сборки. К примеру, при отправке на CI будет собираться именно с Гредлом, который указан в параметрах, а не с тем, который установлен на удалённой машине.
  • Быстрое переключение версии Гредла для проекта.

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

  • Инициализация
    • Анализ settings.gradle.kts, определение, какие проекты включены (include) в сборку
    • Создание экземпляра класса Project для каждого проекта/модуля
  • Конфигурация
    • Анализ сценария сборки из build.gradle.kts каждого проекта
    • Создание направленного ациклического графа задач (DAG) для каждого проекта. Задачи выполняются в порядке их зависимости друг от друга.
  • Выполнение
    • Планирование и выполнение всех задач в соответствии с построенным графом

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

service/gradle.txt · Последнее изменение: 09.04.2024 12:25 — viacheslav

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki