Other code optimizations Gradle Convention Plugins, conclusions based on the results of using the approach
Hi all! Dima Kotikov is in touch, and we are completing a series of articles on how to make your life easier and reduce the boilerplate in gradle files. In previous articles, we prepared and configured a basic module for writing Gradle Convention Plugins, wrote several convention plugins in -.gradle.kts files, made another module and created convention plugins based on kotlin classes. In the final part, we will refactor the written code a little, try to configure the scope of convention plugins and extension functions for the assembly configuration, and also summarize.
Refactoring dependencies in composite builds
There is nothing more constant in development than temporary refactoring. Let's take a look at the contents of the module convention-plugins/base
and we will see that we would not like to give some extension functions and plugins into the hands of users, but would like to use them only when writing custom plugins. For example, basic configuration plugins and extensions like Project.libs
, Project.androidConfig
which are already available in files build.gradle.kts
project modules through generated accessors or due to already connected plugins.
We would not like to lose beautiful extension functions for specifying dependencies for each target and a function for configuring the iOS Framework. You can transfer files easily and painlessly IosExtensions.kt
And KmpDependenciesExtensions.kt
to the module convention-plugins/project
which is what we’ll do:
Now nothing prevents you from removing the module connection convention-plugins/base
from file settings.gradle.kts
root project:
We synchronize the project, try to launch it – everything works. To check that the code from convention-plugins/base
is not available, we are trying to add it to composeApp/build.gradle.kts
plugin android.base.config
and extension function androidConfig
:
We are trying to synchronize the project, launch… And it synchronizes and launches, although we would like to crash. This happens because the plugins from convention-plugins/base
connected to convention-plugins/project
as well as in build.gradle.kts
the root project remained connected to the mock plugin base.plugin
. Let's remove it:
We try to synchronize, start the project – it still works. This is because the plugin connected via includeBuild
transitively gives away its logic. This problem can be partially solved if convention-plugins/project/build.gradle.kts
change the contents of the plugins block:
Then, when you try to build the project, the convention plugins described in the files will no longer be visible -build.gradle.kts
but this still does not solve the problem that extension functions from the module remain visible convention-plugins/base
.
So far I haven't found a way to effectively hide the code from the connected like includeBuild
project without using visibility modifiers on classes. Share in the comments if you know.
You can add a declaration of the entry point to compose-desktop into the general code in case the project contains several app modules. Now at composeApp/build.gradle.kts
it looks like this:
Let's move this into a separate extension – add dependencies on compose plugins in convention-plugins/project/build.gradle.kts
as in convention-plugins/base/build.gradle.kts
and create a separate file – ComposeMultiplatformExtensions.kt
in the module convention-plugins/project
.
To configure an extension, we must understand what exactly to configure. Let's fall into the implementation of the function compose.desktop {}
the generated accessor file will open:
We see that the contents of the function are hidden. Let's try to configure it directly DesktopExtension
which comes to us in the function ComposeExtension.desktop()
. Let's fill it out ComposeMultiplatformExtensions.kt
:
package io.github.dmitriy1892.conventionplugins.project.extensions
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.compose.desktop.DesktopExtension
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
fun Project.composeDesktopApplication(
mainClass: String,
packageName: String,
version: String = libs.versions.appVersionName.get(),
targetFormats: List<TargetFormat> = listOf(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
) {
configure<DesktopExtension> {
application {
this.mainClass = mainClass
nativeDistributions {
targetFormats(*targetFormats.toTypedArray())
this.packageName = packageName
this.packageVersion = version
}
}
}
}
Let's apply our extension to composeApp/build.gradle.kts
:
We try to compile, we see an error:
The error indicates that DesktopExtension
not found in the project. Most likely, it is obtained in another way, but how? To find out, let's go back to the accessor file and decompile it into a java class:
We see that the function input comes ComposeExtension
which comes from Project.extensions
That's why ComposeExtension
called getExtensions()
– and only in these extensions is it configured desktop
. Now we can return to ComposeMultiplatformExtensions.kt
and adjust the internals of our extension function:
package io.github.dmitriy1892.conventionplugins.project.extensions
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.compose.ComposeExtension
import org.jetbrains.compose.desktop.DesktopExtension
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
fun Project.composeDesktopApplication(
mainClass: String,
packageName: String,
version: String = libs.versions.appVersionName.get(),
targetFormats: List<TargetFormat> = listOf(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
) {
extensions.getByType<ComposeExtension>().extensions.configure<DesktopExtension> {
application {
this.mainClass = mainClass
nativeDistributions {
targetFormats(*targetFormats.toTypedArray())
this.packageName = packageName
this.packageVersion = version
}
}
}
}
We try to synchronize – successfully. We assemble the desktop target with the command ./gradlew :composeApp:run
– everything works.
Conclusions, pros and cons of the approach
The time has come to draw a line, point out the advantages and disadvantages of the approach.
Advantages of the approach:
When using convention plugins, file sizes can be significantly reduced
build.gradle.kts
due to the allocation of part of the configurations to plugins and extension functions.Creating and configuring a new module is simplified by using convention plugins with generalized logic.
The main logic with module assembly settings is collected in one place. In our example, in two modules.
Migration to new plugin versions
gradle-wrapper
/agp
/kmp
with any breaking changes it becomes easier, because changes will need to be made point-by-point in specific convention plugins and extension functions instead of a bunch of filesbuild.gradle.kts
modules.
Disadvantages of the approach:
Increased build speed – modules with our plugins, connected as composite builds, must be assembled first, and only then the main project.
When changing generic plugins, there is a risk of breaking the build in modules and other plugins that use it.
To understand what is happening in
build.gradle.kts
– files of the main project, you need to look at what the convention plugin consists of, what is configured in it and connected from external dependencies.Plugins that are written too intricately can take a lot of time to understand.
Conclusions:
It is worth judiciously assessing whether it is necessary to implement convention plugins in a particular project. For pet projects with 1-2 modules, this may be redundant, but for production projects with a large number of modules, the approach will definitely be useful due to the generalization of the logic and the resulting advantages.
It is necessary to break down common parts into plugins so that the modules can be flexibly configured. Plugins for feature modules may be redundant for core/common modules.
You need to be careful about including external dependencies in plugins, since dependencies may not be needed in all modules in which they are connected.
It's important to know when to stop. You shouldn’t overcomplicate the internal implementation of convention plugins.
Stop being afraid of Gradle and get your build scripts in order, it's not that difficult! 🙂
Links to previous parts:
Gradle Convention Plugins: how to make your life easier and reduce boilerplate in gradle files
Creating plugins and reusable parts in .gradle.kts files and Kotlin extension functions
Creating Convention Plugins based on Kotlin classes