Всем привет! На связи Дима Котиков и мы продолжаем разговор о том, как облегчить себе жизнь и уменьшить bolierplate в gradle-файлах. В предыдущих статьях мы сделали отдельный модуль для написания Convention Plugins, провели необходимые настройки и написали несколько Convention Plugin‑ов в «‑.gradle.kts»‑файлах. В этой части мы будем создавать Convention Plugin‑ы на базе Kotlin‑классов.
Создание Convention Plugin-ов в Kotlin-файлах и их регистрация для дальнейшего использования
Чтобы написать convention plugin-ы в Kotlin-файлах, создадим еще один модуль для плагинов и подключим в него модуль base
как композитный. Слишком подробно останавливаться на конфигурации build.gradle.kts
и settings.gradle.kts
для этого модуля я не буду, так как она во многом такая же, как и в модуле base
. Расскажу о нескольких важных моментах.
В файле settings.gradle.kts
модуля project
нужно добавить includeBuild
— подключаем как composite build для того, чтобы модуль base
собрался раньше, чем наш новый модуль, и мы имели возможность использовать ранее созданные convention plugin-ы и extension-функции:
...
versionCatalogs {
create("libs") {
from(files("../../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "project"
includeBuild("../base")
В файле libs.versions.toml
нужно добавить ссылку на наш ранее созданный base
-модуль для подключения в build.gradle.kts
нового модуля. Указываем его без версии:
[libraries]
# Plugins for composite build
gradleplugin-base = { module = "io.github.dmitriy1892.conventionplugins:base" }
В файле build.gradle.kts
модуля project
добавим в блоке dependencies
зависимость на base
-модуль для того, чтобы в новом модуле с плагинами были видны плагины и extension-функции из модуля base
. Помним, что нельзя через блок plugins
добавить плагин в проекте, предназначенном для конфигурации сборки и написания других плагинов:
group = "io.github.dmitriy1892.conventionplugins"
dependencies {
implementation(libs.gradleplugin.android)
implementation(libs.gradleplugin.kotlin)
implementation(libs.gradleplugin.compose)
implementation(libs.gradleplugin.composeCompiler)
// Workaround for version catalog working inside precompiled scripts
// Issue - https://github.com/gradle/gradle/issues/15383
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
implementation(libs.gradleplugin.base)
}
Полный код файлов build.gradle.kts и settings.gradle.kts для нового модуля можно посмотреть по ссылкам. В итоге имеем примерно такую структуру модулей:
Теперь посмотрим на build.gradle.kts
-файл в модуле composeApp
. Видим, что у нас в android-блоке прописан defaultConfig
, который в целом можно вынести в плагин. versionCode
и versionName
тоже можно выделить либо в version catalog, либо в отдельный файл versions.properties.
Обычно с versions.properties
удобнее настраивать CI/CD и автоинкремент сборки, но для этого нужно написать отдельную таску для автоинкремента версии.
Для простоты примера вынесем в version catalog:
Теперь вынесем конфигурацию android application в новый convention plugin, для этого создаем kotlin-файл AndroidApplicationPlugin.kt
в модуле :convention-plugin:project
:
Прописываем класс AndroidApplicationPlugin
, который наследуется от интерфейса org.gradle.api.Plugin
и заполняем:
package io.github.dmitriy1892.conventionplugins.project
import com.android.build.api.dsl.ApplicationDefaultConfig
import io.github.dmitriy1892.conventionplugins.base.extensions.androidConfig
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
class AndroidApplicationPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply(libs.plugins.android.application.get().pluginId)
apply("android.base.config")
apply("android.base.test.config")
}
androidConfig {
defaultConfig {
this as ApplicationDefaultConfig
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = libs.versions.appVersionCode.get().toInt()
versionName = libs.versions.appVersionName.get()
}
}
}
}
}
Мы отнаследовались от Plugin
, в generic-параметр передали Project
— это нужно для того, чтобы сказать gradle-у, что наш класс - плагин и что этот плагин предназначен для gradle-проекта и будет использоваться в build.gradle.kts
-файлах.
Есть возможность написать плагин и для
settings.gradle.kts
, для этого в generic-параметр нужно передатьSettings
, но в этой статье такие плагины не рассматриваются.
Реализовали функцию apply
от интерфейса Plugin
, в ней сконструировали наш скрипт плагина — в блоке with(pluginManager) { ... }
. Этот блок аналогичен блоку plugins {}
в build.gradle.kts
, в него мы прописали плагины, которые включает наш плагин - android application gradle plugin и наши самописные плагины android.base.config
и android.base.test.config
из base-модуля.
По дефолту отсюда недоступен вариант подключения плагинов из version catalog-а через функцию alias()
, как мы это можем делать в обычных build.gradle.kts
-файлах в блоке plugins {}
, поэтому мы через .get().pluginId
подключаем android application gradle plugin плагин в apply()
-функции.
Далее взяли ранее написанный extension androidConfig
и сконфигурировали блок defaultConfig
, взяв из properties поля версий приложения. Теперь, чтобы такой плагин заработал, его нужно зарегистрировать — идем в build.gradle.kts
модуля convention-plugins/project
и указываем внизу файла:
gradlePlugin {
plugins {
register("android.application.plugin") {
id = "android.application.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.AndroidApplicationPlugin"
}
}
}
В первом параметре функции register(name: String, configurationAction: Action<T>)
задаем имя плагина — это внутреннее имя, оно может быть любым, главное — уникальным.
В Action-лямбде задаем id нашего плагина — это тот идентификатор, который будем прописывать в plugins { id(<plugin-id>) }
при подключении плагина. Ну и параметр implementationClass
— это название класса нашего плагина вместе с его package name.
Теперь мы можем заменить еще часть кода в composeApp/build.gradle.kts
-файле на наш плагин:
Пробуем синхронизировать проект:
По информации из ошибки видим, что при применении плагина android.base.test.config
не виден kotlin multiplatform plugin. Это произошло из-за того, что мы в плагин с android-тестами добавили конфигурационный блок kotlinAndroidTarget
, который содержит kotlinMultiplatformConfig
.
В android.application.plugin
мы не подключали KMP-плагин, и поэтому нам выдало ошибку при попытке наш плагин применить. Исправим эту оплошность разделив настройку тестов для android и для kmp. Добавим в convention-plugins/base
новый плагин на базе gradle.kts
-файла, назовем kmp.base.test.config.gradle.kts
, куда и переместим конфигурацию в блоке kotlinAndroidTarget
. Итоговый вид файлов будет таким:
Плагины разделили, подключим плагин kmp.base.test.config
в build.gradle.kts
модулей проекта, чтобы не сломать тесты.
Синхронизируемся, пробуем запустить — все работает!
Идем дальше, сделаем плагин для android library модуля, создаем файл AndroidLibraryPlugin.kt
и наполняем:
package io.github.dmitriy1892.conventionplugins.project
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
class AndroidLibraryPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply(libs.plugins.android.library.get().pluginId)
apply("android.base.config")
apply("android.base.test.config")
}
}
}
}
Регистрируем плагин в build.gradle.kts
модуля convention-plugins/project
:
gradlePlugin {
plugins {
...
register("android.library.plugin") {
id = "android.library.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.AndroidLibraryPlugin"
}
}
}
Подключаем плагин в shared-uikit/build.gradle.kts
-файле и удаляем ставшими ненужными строчки:
Синхронизируемся, запускаем. Видим, что все работает. Далее по такому же принципу напишем KmpComposeApplicationPlugin
:
package io.github.dmitriy1892.conventionplugins.project
import org.gradle.api.Plugin
import org.gradle.api.Project
class KmpComposeApplicationPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("android.application.plugin")
apply("kmp.compose.config")
apply("kmp.base.test.config")
}
}
}
}
И плагин для library-модуля - KmpComposeLibraryPlugin
:
package io.github.dmitriy1892.conventionplugins.project
import org.gradle.api.Plugin
import org.gradle.api.Project
class KmpComposeLibraryPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("android.library.plugin")
apply("kmp.compose.config")
apply("kmp.base.test.config")
}
}
}
}
Зарегистрируем оба плагина в build.gradle.kts
модуля convention-plugins/project
:
gradlePlugin {
plugins {
...
register("kmp.compose.application.plugin") {
id = "kmp.compose.application.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.KmpComposeApplicationPlugin"
}
register("kmp.compose.library.plugin") {
id = "kmp.compose.library.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.KmpComposeLibraryPlugin"
}
}
}
Применяем плагины в build.gradle.kts
модулей проекта и вычищаем ненужное из plugins
-блоков:
Что еще можно улучшить
Можно вынести подключение библиотек в отдельные плагины для компактности и удобства подключения, сделаем плагины для подключения корутин, сериализации, ktor, coil:
1. Kotlin coroutines:
package io.github.dmitriy1892.conventionplugins.project
import io.github.dmitriy1892.conventionplugins.base.extensions.androidMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.commonMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.commonTestDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.jvmMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
class KmpCoroutinesPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
commonMainDependencies {
implementation(libs.kotlinx.coroutines.core)
}
commonTestDependencies {
implementation(libs.kotlinx.coroutines.test)
}
androidMainDependencies {
implementation(libs.kotlinx.coroutines.android)
}
jvmMainDependencies {
implementation(libs.kotlinx.coroutines.swing)
}
}
}
}
2. Kotlin serialization:
package io.github.dmitriy1892.conventionplugins.project
import io.github.dmitriy1892.conventionplugins.base.extensions.commonMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
class KmpSerializationPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply(libs.plugins.kotlinx.serialization.get().pluginId)
}
commonMainDependencies {
implementation(libs.kotlinx.serialization.json)
}
}
}
}
3. Coil:
package io.github.dmitriy1892.conventionplugins.project
import io.github.dmitriy1892.conventionplugins.base.extensions.commonMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
class KmpCoilPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
commonMainDependencies {
implementation(libs.coil)
implementation(libs.coil.network.ktor)
}
}
}
}
4. Ktor:
package io.github.dmitriy1892.conventionplugins.project
import io.github.dmitriy1892.conventionplugins.base.extensions.androidMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.commonMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.iosMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.jvmMainDependencies
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
class KmpKtorPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
commonMainDependencies {
implementation(libs.ktor.core)
}
androidMainDependencies {
implementation(libs.ktor.client.okhttp)
}
jvmMainDependencies {
implementation(libs.ktor.client.okhttp)
}
iosMainDependencies {
implementation(libs.ktor.client.darwin)
}
}
}
}
5. Регистрируем плагины в build.gradle.kts
модуля convention-plugins/project
:
gradlePlugin {
plugins {
...
register("kmp.coroutines.plugin") {
id = "kmp.coroutines.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.KmpCoroutinesPlugin"
}
register("kmp.serialization.plugin") {
id = "kmp.serialization.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.KmpSerializationPlugin"
}
register("kmp.coil.plugin") {
id = "kmp.coil.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.KmpCoilPlugin"
}
register("kmp.ktor.plugin") {
id = "kmp.ktor.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.KmpKtorPlugin"
}
}
}
Применяем полученные плагины в build.gradle.kts
модулей проекта и вычищаем ненужное из plugins
-блоков:
Можем пойти еще дальше: объединить наши кастомные плагины в один и подключать всю пачку одной строкой. Но такой плагин, скорее всего, будет нужен только в рамках нашего конкретного проекта. Это оправдано на многомодульных проектах с одинаковой конфигурацией в модулях, но для нашего примера это скорее будет лишним.
Посмотрим промежуточный результат:
1. Модуль composeApp
, файл build.gradle.kts
:
было: 143 строки кода;
стало: 42 строки кода.
2. Модуль shared-uikit
, файл build.gradle.kts
:
было: 116 строк кода;
стало: 21 строка кода.
Выглядит неплохо: для app-модуля кода почти в 3,5 раза меньше, для library-модуля — в 5,5 раза меньше!
Осталась заключительная часть нашей серии, в ней поговорим о рефакторинге зависимостей в composite builds, подведем итоги и обсудим плюсы и минусы подхода.