A deeper look into Gradles Kotlin DSL

We converted a 613 loc Groovy build script to Kotlin

At the end of last year I already took a look at the Kotlin version of Gradle. Back then I’ve just played around with the Kotlin DSL. I created new *.gradle.kts files and implemented the basics. But I’ve never touched a build file from one of our projects at grandcentrix. Especially not a Gradle file which is in production since 2013!

My colleague Jannis Veerkamp and I got the chance to do it. We were granted some time to convert a build.gradle to a build.gradle.kts file of a production app. And you all know that something like that is always a little bit trickier than just playing around on a green field with new tools or frameworks.


In this post we don’t want to cover “how to convert” but “in which issues we ran” while doing so. If you’re interested in some basics checkout this post.

Applying of plugins

Gradle doesn’t recognize (or knows about) the android block if you use the old apply plugin: 'com.android.application' syntax. There are basically two options to fix that. Either use the new plugins block (recommended) or use configure<com.android.build.gradle.AppExtension> and hope that Google will never move that extension to a new package.

// Apply plugins via the plugins block
plugins {
id("com.android.application")
}
android { ... }
// Alternative use
apply {
plugin("com.android.application")
}
configure<com.android.build.gradle.AppExtension> {
...
}

Dependencies

New dependency syntax

Moving from implementation 'x.y:z:1.0' to implementation("x.y:z:1.0") isn’t that hard, right? Yes, you are right. If you have just a few dependencies. But if you have round a bout 90 dependencies it is really annoying to convert them all.

First we thought we could make use of Kotlins infix-Syntax. After some research we found out that infix needs always three parameters. So doing like the following would be possible, but doesn’t really solve the problem:

dependencies {
this implementation "x.y:z:1.0"
}

Anyway. Thanks to our powerful IDE we can do some cool “Find and Replace”-Scripting which will do the converting for us.

Let the IDE do the work for you

Just press ⌘+R (for macOS users), check the Regex checkbox, put the following into the fields and press Replace all:

// Search
implementation '(.*)'
// Replace
implementation("$1")

You have to do this for all of your configurations of course. If you want to know more about how that works: here is the explanation.

Moving dependencies

We tried to extract the big-fat dependencies block into a separate file. We wanted to end up like this:

.
├── Project
│ ├── build.gradle.kts
│ └── dependencies.gradle.kts

We assumed that we can easily move our current block to that new file and apply it like we can do with any other *.gradle file.

// Project/build.gradle
apply from: "dependencies.gradle.kts"
// Project/dependencies.gradle.kts
dependencies {
implementation("x.y:z:1.0")
testImplementation("y.z:x:1.0")
compileOnly("z.x:y:1.0")
}

Unfortunately that isn’t possible. All of the configurations like implementation and testImplementation etc. can’t be resolved.

We opened a issue at GitHub quickly where we got the answer why that happens and how to solve that. In a nutshell: Kotlin DSL doesn’t know anything about these configuration because they aren’t available at script time. They will be added later at runtime.

We solved that by delegating these configurations from the ConfigurationHandler.

// Project/dependencies.gradle.kts
val implementation by configurations
val testImplementation by configurations
val compileOnly by configurations
dependencies {
implementation("x.y:z:1.0")
testImplementation("y.z:x:1.0")
compileOnly("z.x:y:1.0")
}

BuildTypes and ProductFlavors

Each app has at least two buildTypes: release and debug. Both get generated by the AGP (Android Gradle Plugin) for you. Nowadays additional buildTypes or productionFlavors aren’t rare. And of course, even our app have some. The Groovy version looks like that:

productFlavors {
play { ... }
}

Since we are using Kotlin defining a string “somewhere” (play) followed by a lambda isn’t possible anymore. See the following example — which is not a valid statement in Kotlin:

productFlavors {
"play" { ... }
}

After taking a look into the current AGP DSL Reference we found out that productFlavors block delegates to a NamedDomainObjectContainer (whatever it is 😅). But as we saw in that documentation it provides methods like create(String) or getByName(String) (to be fair: the last one is a little bit hidden in a subclass). Means we have to call — based on if a buildType or productFlavor already exist — the create or getByName method:

buildTypes {
getByName("debug") { ... }
}
productFlavors {
create("play") { ... }
}

Methods and properties

Sometimes we didn’t really know if we have to call a method or set a property. That arises from the fact that you can omit the parentheses from a method call in Groovy while you can use the property access syntax in Kotlin.

// Groovy version
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1"
// Kotlin version
minSdkVersion(21)
targetSdkVersion(27)
versionCode = 1
versionName = "1"

As you see from the example above — have you expected that you have to call *SdkVersion methods but setting the version* via the property syntax?
That is like it is because the signature of the *SdkVersion method takes a ApiVersion class as parameter.

Using the property access syntax is not possible

But there is a helper method called targetSdkVersion(Int) which wraps the Int to a ApiVersion instance.

Extras

You may know that Gradle provides an ExtraPropertiesExtension for each project. If you use Kotlin for your Android project you probably use that already. The default build.gradle (root and project) files for new projects look something like the following:

// root build.gradle
buildscript {
ext.kotlin_version = '1.2.40'
...
}
// project build.gradle
dependencies {
implementation "org.jetbrains.kotlin:kotlinstdlib:$kotlin_version"
}

The Kotlin DSL makes that way more understandable for us. You can simply get an instance of that extension by calling extra. Then you can call the set(String, Any) resp. get(String) to set or get a property.

Beside of that. The Kotlin DSL provides a delegate extension method which can be called to get an extra property. Note that the name of the property has to match the name of the property inside the extras.

// root build.gradle.kts
buildscript {
exta.set("kotlinVersion", "1.2.40")
...
}
// project build.gradle.kts
dependencies {
// either use the `get` method
val kotlinVersion = rootProject.extra.get("kotlinVersion")
// or use the delegate method
val kotlinDelegateVersion: String by rootProject.extra
implementation("org.jetbrains.kotlin:kotlinstdlib:$kotlinVersion")
}

Conclusion

In my previous post about Kotlin in Gradle scripts I recommended — more or less — to don’t use it in production yet. As I have learned a lot about the Kotlin DSL — and all about Gradle themself — I changed my mind now. From my point of view you can try it out in your projects.

But note there is still a lot of red code and bugs in the IDE. There is still space for improvements and if you aren’t familiar with Gradle like me and don’t have time to switch stick at the Groovy DSL.