DEV Community

Cover image for Understanding (a bit of) the Gradle Kotlin DSL
Antonio Feregrino
Antonio Feregrino

Posted on

Understanding (a bit of) the Gradle Kotlin DSL

Ever since I started learning Kotlin I have been intrigued by what is going on behind the scenes in the build.gradle.kts file. At first glance, the file looks confusing, and for a newbie like me, it is hard to comprehend how the snippet below even has Kotlin-valid syntax.

plugins {
  kotlin("jvm") version "1.9.25"
  kotlin("plugin.spring") version "1.9.25"
  id("org.springframework.boot") version "3.4.1"
}
Enter fullscreen mode Exit fullscreen mode

To fully understand how is this Kotlin there are a couple of mechanisms at play here that you must understand.

  • Lambdas with receivers
  • Infix functions

What is plugins?

Internally, plugins is a function whose signature looks somewhat like this:

fun plugins(block: PluginDependenciesSpecScope.() -> Unit): Unit
Enter fullscreen mode Exit fullscreen mode

This tells us that plugins is a function that takes a single argument named block which known as a lambda with receiver.

A lambda with receiver is a special kind of lambda that allows you to call methods on a specific object (the receiver) within the lambda's body without explicitly referencing it.

For example, in our function signature above, we can see that the receiving type is PluginDependenciesSpecScope from the package org.gradle.kotlin.dsl. This lambda function does not receive any arguments, as indicated by the empty parentheses after the receiver type specification .(), and it does not return anything meaningful, as indicated by the -> Unit.

A potential implementation of the plugins function could be:

fun plugins(block: PluginDependenciesSpecScope.() -> Unit): Unit {
    val scope = PluginDependenciesSpecScope()
    scope.block()
}
Enter fullscreen mode Exit fullscreen mode

While Gradle is way more complex, this is just a very simple example of how the function could be implemented. The key part is that behind the scenes, an instance of PluginDependenciesSpecScope is created for you, and block, being an extension method, can be called directly on this instance.

What is kotlin and id?

Both kotlin and id are extension methods of PluginDependenciesSpecScope, the reason behind one being able to call these methods directly without specifying the receiver is because this receiver is implicit, but if you wanted to, you reference it directly using the this keyword:

plugins {
  this.kotlin("jvm") version "1.9.25"
  this.kotlin("plugin.spring") version "1.9.25"
  this.id("org.springframework.boot") version "3.4.1"
}
Enter fullscreen mode Exit fullscreen mode

Both these methods follow the fluent pattern, meaning they return the same object they acted on. For example, the signature of the kotlin method looks like this:

fun PluginDependenciesSpec.kotlin(module: String): PluginDependencySpec
Enter fullscreen mode Exit fullscreen mode

The difference between kotlin and id

Behind the scenes, kotlin is just a shorthand for id, when you call kotlin("jvm"), this methods calls id but appends the prefix org.jetbrains.kotlin. to whatever string you passed, so kotlin("jvm") is equal to id("org.jetbrains.kotlin.jvm").

So, what is version?

The missing piece in my understanding of the Kotlin Gradle DSL is the version in the plugin definition, at first I thought this was a keyword, however, it turns out is an extension method, but not any kind of method but an infix one.

The signature of the version method looks like:

infix fun PluginDependencySpec.version(version: String): PluginDependencySpec
Enter fullscreen mode Exit fullscreen mode

As you can see, it takes operates on a PluginDependencySpec but takes in a String and returns a PluginDependencySpec.

An infix function can be called without using dot notation and parentheses. It's declared using the infix keyword and must have a single parameter. This allows for more readable, natural language-like expressions in code.

So we could further change our plugin block definition to:

plugins {
  this.kotlin("jvm").version("1.9.25")
  this.kotlin("plugin.spring").version("1.9.25")
  this.id("org.springframework.boot").version("3.4.1")
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

On Lambdas with receivers

While the approach of using lambdas with receivers is powerful for creating DSLs, it can initially be confusing for those new to Kotlin or Gradle. This technique allows for a more natural, declarative syntax, enabling code that looks like special language constructs when it's actually just method calls on an implicit receiver.

Once grasped, this results in more readable and expressive code that closely resembles the domain it's describing - in this case, Gradle build configurations. However, the implicit nature of the receiver can be a source of confusion for beginners trying to understand what's happening "behind the scenes.”

On fluent interfaces and infix methods

The fluent pattern and infix methods aim to create a chain of method calls that read almost like natural language. In theory, this should enhance readability and make the build script more intuitive. However, for those not deeply familiar with Gradle, Kotlin, or the concept of DSLs, this syntax can initially seem magical or confusing.

It's not immediately obvious that version is a method, for instance, or why we can omit parentheses in some places but not others. While it allows for a clear separation of concerns, each method in the chain responsible for a specific aspect of plugin configuration requires some learning and adjustment to fully appreciate and utilise effectively.

As for me…

While I'm still getting accustomed to the Gradle Kotlin DSL and occasionally find it confusing, I've come to appreciate its power and elegance. Building DSLs is indeed one of Kotlin's strengths, and understanding these concepts opens up exciting possibilities for creating expressive and maintainable code.

By exploring the mechanics behind Gradle's Kotlin DSL, I feel I have demystified a bit of its syntax and gained insights into advanced Kotlin features that can be applied in various contexts. When mastered, these patterns and techniques can significantly enhance our ability to write clean, readable, and powerful code.

I hope this dive into the Gradle Kotlin DSL has been as enlightening for you as it has been for me. Whether you're a seasoned Kotlin developer or just starting out, understanding these concepts can greatly improve your grasp of Gradle and Kotlin's capabilities.

Top comments (0)