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