
Всем привет! На связи Дима Котиков и мы продолжаем разговор о том, как облегчить себе жизнь и уменьшить 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 для нового модуля можно посмотреть по ссылкам. В итоге имеем примерно такую структуру модулей:

plugins для будущих convention plugin-овТеперь посмотрим на build.gradle.kts-файл в модуле composeApp. Видим, что у нас в android-блоке прописан defaultConfig, который в целом можно вынести в плагин. versionCode и versionName тоже можно выделить либо в version catalog, либо в отдельный файл versions.properties. Обычно с versions.properties удобнее настраивать CI/CD и автоинкремент сборки, но для этого нужно написать отдельную таску для автоинкремента версии.

build.gradle.kts-файл модуля composeAppДля простоты примера вынесем в version catalog:

libs.versions.tomlТеперь вынесем конфигурацию android application в новый convention plugin, для этого создаем kotlin-файл AndroidApplicationPlugin.kt в модуле :convention-plugin:project:

AndroidApplicationPlugin.ktПрописываем класс 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.application.plugin
Пробуем синхронизировать проект:

По информации из ошибки видим, что при применении плагина 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-файле и удаляем ставшими ненужными строчки:

android.library.pluginСинхронизируемся, запускаем. Видим, что все работает. Далее по такому же принципу напишем 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-блоков:

build.gradle.kts модулей проектаЧто еще можно улучшить
Можно вынести подключение библиотек в отдельные плагины для компактности и удобства подключения, сделаем плагины для подключения корутин, сериализации, 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, подведем итоги и обсудим плюсы и минусы подхода.