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