Creating Convention Plugins Based on Kotlin Classes
Hello everyone! Dima Kotikov is here and we continue our conversation about how to make your life easier and reduce the bolierplate in gradle files. In the previous articles, we made a separate module for writing Convention Plugins, made the necessary settings and wrote several Convention Plugins in “-.gradle.kts” files. In this part, we will create Convention Plugins based on Kotlin classes.
Creating Convention Plugins in Kotlin files and registering them for further use
To write convention plugins in Kotlin files, let's create another module for plugins and include the module in it. base
as a composite. It is too much to dwell on the configuration build.gradle.kts
And settings.gradle.kts
for this module I won't, since it is in many ways the same as in the module base
I will tell you about several important points.
In the file settings.gradle.kts
module project
need to add includeBuild
— we connect as a composite build so that the module base
was built before our new module, and we were able to use previously created convention plugins and extension functions:
...
versionCatalogs {
create("libs") {
from(files("../../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "project"
includeBuild("../base")
In the file libs.versions.toml
we need to add a link to our previously created base
-module for connection in build.gradle.kts
new module. We specify it without the version:
[libraries]
# Plugins for composite build
gradleplugin-base = { module = "io.github.dmitriy1892.conventionplugins:base" }
In the file build.gradle.kts
module project
let's add it to the block dependencies
dependence on base
– module so that in the new module with plugins the plugins and extension functions from the module are visible base
. Remember that you can't go through the block plugins
add a plugin to the project intended for configuring the build and writing other 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)
}
Full code of files build.gradle.kts And settings.gradle.kts for the new module you can look at the links. As a result, we have approximately the following module structure:
Now let's look at build.gradle.kts
-file in module composeApp
. We see that we have it registered in the android block defaultConfig
which can generally be moved to a plugin. versionCode
And versionName
can also be allocated either in the version catalog or in a separate file versions.properties.
Usually with versions.properties
It is more convenient to set up CI/CD and auto-increment of the build, but for this you need to write a separate task for auto-increment of the version.
To simplify the example, let's put it in the version catalog:
Now we will move the android application configuration to a new convention plugin, for this we will create a kotlin file AndroidApplicationPlugin.kt
in the module :convention-plugin:project
:
We register the class AndroidApplicationPlugin
which inherits from the interface org.gradle.api.Plugin
and fill in:
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()
}
}
}
}
}
We inherited from Plugin
passed to the generic parameter Project
— this is needed to tell gradle that our class is a plugin and that this plugin is intended for a gradle project and will be used in build.gradle.kts
-files.
It is possible to write a plugin for
settings.gradle.kts
for this you need to pass in the generic parameterSettings
but such plugins are not discussed in this article.
Implemented the function apply
from the interface Plugin
in it we constructed our plugin script – in the block with(pluginManager) { ... }
This block is similar to the block plugins {}
V build.gradle.kts
we have registered plugins in it, which include our plugin – android application gradle plugin and our own plugins android.base.config
And android.base.test.config
from the base module.
By default, the option to connect plugins from the version catalog via the function is not available from here. alias()
how can we do this in ordinary build.gradle.kts
-files in the block plugins {}
so we through .get().pluginId
connect android application gradle plugin plugin in apply()
-functions.
Next we took the previously written extension androidConfig
and configured the block defaultConfig
taking the application version fields from properties. Now, for such a plugin to work, it needs to be registered – go to build.gradle.kts
module convention-plugins/project
and indicate at the bottom of the file:
gradlePlugin {
plugins {
register("android.application.plugin") {
id = "android.application.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.AndroidApplicationPlugin"
}
}
}
In the first parameter of the function register(name: String, configurationAction: Action<T>)
we set the name of the plugin – this is the internal name, it can be anything, the main thing is that it is unique.
In the Action lambda we set the id of our plugin – this is the identifier that we will write in plugins { id(<plugin-id>) }
when connecting the plugin. Well, the parameter implementationClass
— this is the name of our plugin class together with its package name.
Now we can replace some more code in composeApp/build.gradle.kts
-file to our plugin:
Let's try to synchronize the project:
According to the information from the error, we see that when using the plugin android.base.test.config
kotlin multiplatform plugin is not visible. This happened because we added a configuration block to the android tests plugin kotlinAndroidTarget
which contains kotlinMultiplatformConfig
.
IN android.application.plugin
we didn't connect the KMP plugin, and that's why we got an error when trying to apply our plugin. We'll fix this by separating the test setup for android and kmp. We'll add to convention-plugins/base
new plugin based on gradle.kts
-file, let's call it kmp.base.test.config.gradle.kts
where we will move the configuration in the block kotlinAndroidTarget
. The final appearance of the files will be as follows:
Plugins are separated, let's connect the plugin kmp.base.test.config
V build.gradle.kts
project modules so as not to break the tests.
We synchronize, try to launch – everything works!
Let's go further, let's make a plugin for the android library module, create a file AndroidLibraryPlugin.kt
and fill in:
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")
}
}
}
}
Register the plugin in build.gradle.kts
module convention-plugins/project
:
gradlePlugin {
plugins {
...
register("android.library.plugin") {
id = "android.library.plugin"
implementationClass = "io.github.dmitriy1892.conventionplugins.project.AndroidLibraryPlugin"
}
}
}
Connect the plugin in shared-uikit/build.gradle.kts
-file and delete the lines that have become unnecessary:
We synchronize, launch. We see that everything works. Next, we write according to the same principle 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")
}
}
}
}
And a plugin for the library module – 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")
}
}
}
}
Let's register both plugins in build.gradle.kts
module 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"
}
}
}
We use plugins in build.gradle.kts
project modules and clean out unnecessary parts plugins
-blocks:
What else can be improved?
You can move the connection of libraries to separate plugins for compactness and ease of connection, we will make plugins for connecting coroutines, serialization, 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. Who:
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. Register plugins in build.gradle.kts
module 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"
}
}
}
We apply the received plugins in build.gradle.kts
project modules and clean out unnecessary parts plugins
-blocks:
We can go even further: combine our custom plugins into one and connect the whole bunch with one line. But such a plugin will most likely only be needed within the framework of our specific project. This is justified in multi-module projects with the same configuration in modules, but for our example it will most likely be unnecessary.
______________________
Let's look at the intermediate result:
1. Module composeApp
file build.gradle.kts
:
2. Module shared-uikit
file build.gradle.kts
:
It looks good: for the app module there is almost 3.5 times less code, for the library module – 5.5 times less!
There is still the final part of our series left, in which we will talk about dependency refactoring in composite builds, summarize and discuss the pros and cons of the approach.