Gradle is the glue that binds our code together that allows us to build an Android application. Exposure to Gradle can range from limited to deep knowledge producing plugins. Mine currently sits somewhere in the middle. I’m currently working on levelling it.
My topic of focus over the past few weeks has been Convention Plugins. This post is the culmination of what I’ve learned and it helps me frame Convention Plugins for my mental model. This is by no means a solid resource!
Multi-project applications are nearing the standard for Android codebases*. Sharing your build logic and rules across modules is important for a number of reasons:
Here are concrete examples to help:
You get the idea. If you’ve been around for a while these might stick out as issues you have had to solve. Convention Plugins can help us solve these problems with idiomatic Gradle.
The majority of my experience with this in the past has been to apply scripts in relevant modules, use subprojects
or allprojects
blocks or put logic in buildSrc
.
I am used to working with complicated Gradle files. Which can make fixing issues miserable! Does this look familiar?
plugins {
id("kotlin-android")
id("kotlin-kapt")
id("com.android.library")
id("dagger.hilt.android.plugin")
}
android {
minSdk 30
defaultConfig {
minSdk = 21
targetSdk = 30
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8.toString()
targetCompatibility = JavaVersion.VERSION_1_8.toString()
}
kotlinOptions {
jvmTarget = '1.8'
}
}
You may then have a similar Gradle file across many projects. Moving to Java 11 suddenly becomes a manual process. You can ease the pain by using project wide variables to hold values. You can remove the pain with a Convention Plugin.
A convention plugin allows us to define configurations, or conventions, for builds that we re-use across a project.
A convention is represented by a Gradle script or a Plugin. They will live in a build logic module that will register plugins with Gradle. The module is then applied via your pluginManagement
.
pluginManagement {
includeBuild("build-logic")
}
Once included all projects can access your plugin.
But what can a convention plugin do? Anything a normal Gradle script or Plugin can do!
One of the real benefits to me is that it makes Gradle build files feel more life software. You can write plugins using apply bread and butter principles like: cohesion, coupling and composition. You can write tests, making your build file feel predictable.
Here’s a simple example of how we can create a Kotlin convention:
class KotlinConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply("org.jetbrains.kotlin.jvm")
target.tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
}
We can then add this to the build using the build.gradle.kts
file in our build-logic
module.
gradlePlugin {
plugins {
register("kotlinApplication") {
id = "example.kotlin"
implementationClass = "KotlinConventionPlugin"
}
}
}
A project can then apply this like any other plugin.
plugins {
id("example.kotlin)
}
If you want to update the JVM target you can do that in a single file and have all projects update.
On its own, a Kotlin plugin is less exciting. I think a compelling use case is when we think about applying an annotation processor and then the libraries that use it.
When creating conventions we should split conventions logically. For example, if we want to use hilt in a project.
Hilt is a dependency injection library that uses kapt. We can write a convention plugin as follows:
class HiltConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("org.jetbrains.kotlin.kapt")
apply("dagger.hilt.android.plugin")
}
dependencies {
add("implementation", "com.google.dagger:hilt-android:2.44")
add("kapt", "com.google.dagger:hilt-android-compiler:2.44")
}
}
}
This library applies the kapt plugin, hilt plugin and adds the related dependencies. The dependencies here are hard coded as an example but you should make use of the VersionCatalog extension like this:
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
"implementation"(libs.findLibrary("hilt.android").get())
"kapt"(libs.findLibrary("hilt.compiler").get())
}
We can register the plugin:
register("hiltConvention") {
id = "example.hilt"
implementationClass = "HiltConventionPlugin"
}
Then apply it:
plugins {
id("example.hilt")
}
Now, updating the version or swapping from kapt
to ksp
only needs a developer to change a single plugin. Not many projects.
This scratches the surface. There are many incredible resources out there to help you get started.