Создание Convention Plugin-ов на базе Kotlin-классов

от автора

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

Структура модулей с добавленным модулем plugins для будущих convention plugin-ов

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

build.gradle.kts-файл модуля composeApp

build.gradle.kts-файл модуля composeApp

Для простоты примера вынесем в version catalog:

Файл libs.versions.toml

Файл libs.versions.toml

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

Файл AndroidApplicationPlugin.kt

Файл 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.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

Подключение плагина 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-блоков:

Применение kmp-плагинов в build.gradle.kts модулей проекта

Применение kmp-плагинов в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *