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.androidConfigwhich 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/projectwhich is what we’ll do:

Moving IosExtensions.kt and KmpDependenciesExtensions.kt to the convention-plugins/project module

Transfer IosExtensions.kt And KmpDependenciesExtensions.kt to the module convention-plugins/project

Now nothing prevents you from removing the module connection convention-plugins/base from file settings.gradle.kts root project:

Excluding the convention-plugins/base module from the root project's settings.gradle.kts

Module exception convention-plugins/base from 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:

Checking that convention-plugins/base is disabled

Checking the shutdown convention-plugins/base

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/projectas well as in build.gradle.kts the root project remained connected to the mock plugin base.plugin. Let's remove it:

Removing base.plugin from root project's build.gradle.kts

Removal base.plugin from build.gradle.kts root project

We try to synchronize, start the project – it still works. This is because the plugin connected via includeBuildtransitively gives away its logic. This problem can be partially solved if convention-plugins/project/build.gradle.kts change the contents of the plugins block:

Changing the configuration of the plugins block in convention-plugins/project/build.gradle.kts

Changing the configuration of the plugins block in convention-plugins/project/build.gradle.kts

Then, when you try to build the project, the convention plugins described in the files will no longer be visible -build.gradle.ktsbut 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:

Declaring the entry point for the desktop target

Declaring the entry point for the desktop target

Let's move this into a separate extension – add dependencies on compose plugins in convention-plugins/project/build.gradle.ktsas in convention-plugins/base/build.gradle.ktsand 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:

Accessor file with the generated function compose.desktop {} and extension property

Accessor file with the generated function compose.desktop {} and extension property

We see that the contents of the function are hidden. Let's try to configure it directly DesktopExtensionwhich 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:

Using composeDesktopApplication in composeApp/build.gradle.kts

Application composeDesktopApplication V composeApp/build.gradle.kts

We try to compile, we see an error:

DesktopExtension configuration error

Configuration error DesktopExtension

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:

Path to the tool for decompiling a kotlin file in java

Path to the tool for decompiling a kotlin file in java

Decompiled accessor

Decompiled accessor

We see that the function input comes ComposeExtensionwhich comes from Project.extensionsThat'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 files build.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:

  1. Gradle Convention Plugins: how to make your life easier and reduce boilerplate in gradle files

  2. Creating plugins and reusable parts in .gradle.kts files and Kotlin extension functions

  3. Creating Convention Plugins based on Kotlin classes

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *