Dependencies versions in Gradle Kotlin DSL
Migrating Gradle scripts from Groovy to Kotlin can be exciting and satisfying. Especially for those projects where Kotlin is the source code language of choice. Probably like most Android developers, I learn Groovy syntax only for sake of making changes to build process, necessary minimum. Now when I can play with Kotlin syntax I feel I understand more from Gradle.

One of commons tasks, when configuring Gradle build, is defining dependencies for our project. With multi-module structure often we repeat dependencies in multiple modules:
// module-a/build.gradle.kts
dependencies {
implementation("androidx.appcompat:appcompat:1.1.0")
}// module-b/build.gradle.kts
dependencies {
implementation("androidx.appcompat:appcompat:1.1.0")
}
Issue we are dealing here with is defining library version twice. It is not a good practice since it may lead to mismatch in future updates (when forgetting updating one place) and in consequence unpredicted behaviour on runtime. We need single source of truth and use the same versions throughout the project. Lets look at different ways you can solve this issue…
buildSrc directory
buildSrc
is a directory in your project structure which Gradle automatically compiles and puts it in the classpath of your build scripts. This makes it a good candidate for defining common logic and allows for declaration to be available over all project modules. You don’t even have to put any import or apply from:
in order to have the visibility on the defined values. You can read more in the documentation here.
In short, what is needed:
Step 1: Define values in any Kotlin file under buildSrc/src/main/kotlin
const val appCompatVersion = "1.1.0"
Step 2: Refer to it in any build.gradle.kts
file
dependencies {
implementation("androidx.appcompat:appcompat:$appCompatVersion")
}
When you set this up it works like a charm for this purpose. From my research, when project uses Gradle Kotlin DSL, this is the most popular way for defining dependencies versions (often combined with dependencies names next to it). Additionally, you can place custom tasks or binary plugins there if you have such.
You can look closer on all the required changes in this sample commit.
Side note: versions defined this way (and other ways presented below) won’t be visible for
buildSrc/build.gradle.kts
script, so we can’t have every dependency versions in one common place.
Also, creating directory structure in a separate buildSrc
module only to achieve this simple task, feels like an overkill. Lets take a look at other approaches.
extra properties in separate build script
I got used to having versions defined in separate dependencies.gradle
file. We can add properties to extra properties extensions to be later read in another script file (and reused). Groovy is a dynamic language and allows for referring those values by names:
ext {
appCompatVersion = '1.1.0'
}dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVersion"
}
Kotlin on the other hand, is statically typed so it won’t allow for such shortcuts. This is unfortunate for our needs, but because of the same reason, we gain access to such features like refactor or auto-complete in the IDE.
Lets see how we can achieve similar result when using Kotlin scripts:
Step 1: Define values in versions.gradle.kts
and put them into extra
mapOf(
"appCompatVersion" to "1.1.0"
).forEach { (name, version) ->
project.extra.set(name, version)
}
Step 2: Refer to it in any build.gradle.kts
file
apply(from = "../versions.gradle.kts")
val appCompatVersion: String by extradependencies {
implementation("androidx.appcompat:appcompat:$appCompatVersion")
}
In order for it to work we need to declare local val
property and match its name with the name
given as key for extra
properties extension. Luckily, extra
has delegated property defined already, so we don’t have to deal with manually scanning map structure by its key. One important note here is that the property need to have String
type declared, or else it won’t compile.
Unfortunately, if there are a lot of dependencies in particular module, there will be a lot of local properties defined. On top of that we need to apply versions
script file. And the worst part: because we are referring to defined versions by key names which are String
s, IDE won’t help us with navigating to associated values 😢.
Full picture of necessary changes can be reviewed in this sample commit.
read from properties file
Another approach we can take is to put our version values into properties file so they can be easily accessed:
Step 1: Define values in gradle.properties
appCompatVersion=1.1.0
Step 2: Refer to it in any build.gradle.kts
file
val appCompatVersion: String by projectdependencies {
implementation("androidx.appcompat:appcompat:$appCompatVersion")
}
Again, we can’t simply refer the property directly by its name. This time we use project
delegated property to read values from gradle.properties
. In order for it to work, local val
property name must match with the one defined in properties file, and be of type String
. In this approach we can skip apply from
so one line less to write.
If you’re wondering whether to put the values in a separate properties file (i.e. versions.properties
) that is possible, but would require (small) additional logic for reading the file.
Required changes in this approach can be reviewed in this sample commit.
Summary
Those are definitely not the only options you have. Just those which came into my mind when playing with Gradle scripts after migrating to Kotlin DSL. I won’t decide which approach is the best for you, but Gradle engineers recommends to put such values in buildSrc
.
As a general rule: use the one that makes the most sense for your project. Look for good compromise between less code lines and maintainable, clean code structure. No matter which approach you will take, to me it is very joyful experience to have IDE support and more understanding of scripts syntax.