This guide will walk you through converting your Groovy-based Gradle build scripts to Kotlin.

Gradle’s newer Kotlin DSL provides a pleasant editing experience in supported IDEs: content-assist, refactoring, documentation, and more.

IntelliJ IDEA and Android Studio

Before you start migrating

Please read: It’s helpful to understand the following important information before you migrate:

  • Using the latest versions of Gradle, applied plugins, and your IDE should be your first move.

  • Kotlin DSL is fully supported in Intellij IDEA and Android Studio. Other IDEs, such as Eclipse or NetBeans, do not yet provide helpful tools for editing Gradle Kotlin DSL files, however, importing and working with Kotlin DSL-based builds work as usual.

  • In IntelliJ IDEA, you must import your project from the Gradle model to get content-assist and refactoring tools for Kotlin DSL scripts.

  • There are some situations where the Kotlin DSL is slower. First use, on clean checkouts or ephemeral CI agents for example, are known to be slower. The same applies to the scenario in which something in the buildSrc directory changes, which invalidates build-script caching. In addition, IntelliJ IDEA or Android Studio might spawn up to 3 Gradle daemons when editing scripts; one per each type of script: project scripts, settings scripts and initialization scripts. On some builds with slow configuration time (please check out the performance guide) the IDE may feel unresponsive when editing scripts.

  • You must run Gradle with Java 8 or higher. Java 7 is not supported.

  • The embedded Kotlin compiler is known to work on Linux, macOS, Windows, Cygwin, FreeBSD and Solaris on x86-64 architectures.

  • Knowledge of Kotlin syntax and basic language features is very helpful. The Kotlin reference documentation and Kotlin Koans should be useful to you.

  • Use of the plugins {} block to declare Gradle plugins significantly improves the editing experience, and is highly recommended. Consider adopting it in your Groovy build scripts before converting them to Kotlin.

  • The Kotlin DSL will not support model {} elements. This is part of the discontinued Gradle Software Model.

If you run to trouble or a suspected bug, please take advantage of the gradle/kotlin-dsl issue tracker.

You don’t have to migrate all at once! Both Groovy and Kotlin-based build scripts can apply other scripts of either language. You can find inspiration for any Gradle features not covered in the Kotlin DSL samples.

Script file naming

Groovy DSL script files use the .gradle file name extension.

Kotlin DSL script files use the .gradle.kts file name extension.

To use the Kotlin DSL, simply name your files build.gradle.kts instead of build.gradle.

The settings file, settings.gradle, can also be renamed settings.gradle.kts.

In a multi-project build, you can have some modules using the Groovy DSL (with build.gradle) and others using the Kotlin DSL (with build.gradle.kts).

On top of that, apply the following conventions for better IDE support:

  • Name scripts that are applied to Settings according to the pattern *.settings.gradle.kts,

  • Name init scripts according to the pattern *.init.gradle.kts.

Applying plugins

Just like with the Groovy DSL, there are two ways to apply Gradle plugins:

  • declaratively, using the plugins {} block,

  • imperatively, using the apply(..) functions.

Here’s an example using the declarative plugins {} block:

build.gradle
plugins {
    id 'java'
    id 'jacoco'
    id 'maven-publish'
    id 'org.springframework.boot' version '2.0.2.RELEASE'
}
build.gradle.kts
plugins {
    java
    jacoco
    `maven-publish`
    id("org.springframework.boot") version "2.0.2.RELEASE"
}

The Kotlin DSL provides property extensions for all Gradle core plugins, as shown above with the java, jacoco or maven-publish declaration.

Third party plugins can be applied the same way as with the Groovy DSL. Except for the double quotes and parentheses. You can also apply core plugins with that style. But the statically-typed accessors are recommended since they are type-safe and will be autocompleted by your IDE.

You can also use the imperative apply syntax, but then non-core plugins must be included on the classpath of the build script:

build.gradle
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath('org.springframework.boot:spring-boot-gradle-plugin:2.0.2.RELEASE')
    }
}

apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'org.springframework.boot'
build.gradle.kts
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.2.RELEASE")
    }
}

apply(plugin = "java")
apply(plugin = "jacoco")
apply(plugin = "org.springframework.boot")

We strongly recommend that you use the plugins {} block in preference to the apply() function.

The declarative nature of the plugins {} block enables the Kotlin DSL to provide type-safe accessors to the extensions, configurations and other features contributed by the applied plugins, which makes it easy for IDEs to discover the details of the plugins' models and makes them easy to configure.

Configuring plugins

Many plugins come with extensions to configure them. If those plugins are applied using the declarative plugins {} block, then Kotlin extension functions are made available to configure their extension, the same way as in Groovy. The following sample shows how this works for the Jacoco Plugin.

build.gradle
plugins {
    id 'jacoco'
}

jacoco {
    toolVersion = '0.8.1'
}
build.gradle.kts
plugins {
    jacoco
}

jacoco {
    toolVersion = "0.8.1"
}

By contrast, if you use the imperative apply() function to apply a plugin, then you will have to use the configure<T>() function to configure that plugin. The following sample shows how this works for the Checkstyle Plugin by explicitly declaring the plugin’s extension class — CheckstyleExtension — in the configure<T>() function:

build.gradle
apply plugin: "checkstyle"

checkstyle {
    maxErrors = 10
}
build.gradle.kts
apply(plugin = "checkstyle")

configure<CheckstyleExtension> {
    maxErrors = 10
}

Again, we strongly recommend that you apply plugins declaratively via the plugins {} block.

Knowing what plugin-provided extensions are available

Because your IDE knows about the configuration elements that a plugin provides, it will include those elements when you ask your IDE for suggestions. This will happen both at the top level of your build scripts — most plugin extensions are added to the Project object — and within an extension’s configuration block.

You can also run the :kotlinDslAccessorsReport task to learn about the extensions contributed by all applied plugins. It prints the Kotlin code you can use to access those extensions and provides the name and type of the accessor methods.

If the plugin you want to configure relies on groovy.lang.Closure in its method signatures or uses other dynamic Groovy semantics, more work will be required to configure that plugin from a Kotlin DSL build script. See the Interoperability section for more information on how to call Groovy code from Kotlin code or to keep that plugin’s configuration in a Groovy script.

Plugins also contribute tasks that you may want to configure directly. This topic is covered in the Configuring tasks section.

Keeping build scripts declarative

Plugins fetched from a source other than the Gradle Plugin Portal may or may not be usable with the plugins {} block. It depends on how they have been published.

For example, the Android Plugin for Gradle is not published to the Gradle Plugin Portal and at least up to version 3.1.0 of the plugin, the metadata required to resolve the artifacts for a given plugin identifier is not published to the Google repository.

Because of this, projects typically apply the plugin via a buildscript {} block and the apply() function. However, you can use the plugins {} block if you configure your project appropriately. We will show you in this section how to do that for the Android Plugin for Gradle.

When publishing plugins, please use Gradle’s built-in Java Gradle Plugin Plugin. It automates the publication of the metadata necessary to make your plugins usable with the plugins {} block.

The goal is to instruct your build on how to map the com.android.application plugin identifier to a resolvable artifact. This is done in two steps:

  • Add a plugin repository to the build’s settings file

  • Map the plugin ID to the corresponding artifact coordinates

The following example shows you how to add a plugin repository to the build via a pluginManagement {} block in the settings file:

settings.gradle.kts
pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
    }
}

Providing the ID-to-artifact mapping is also done in the settings file, again within the pluginManagement {} block. The following sample maps the com.android.application ID to the com.android.tools.build:gradle:<version> artifact coordinate:

settings.gradle.kts
pluginManagement {
    // ...
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "com.android.application") {
                useModule("com.android.tools.build:gradle:${requested.version}")
            }
        }
    }
}

Once you have completed both steps, you can apply the plugin using the plugins {} block and benefit from the type-safe plugin extension accessors in your build files:

build.gradle.kts
plugins {
    id("com.android.application") version "3.1.0"   (1)
}

android {                                           (2)
    buildToolsVersion("27.0.3")
    compileSdkVersion(27)
}
1 Declaratively request the com.android.application plugin
2 Configure the android extension using Kotlin DSL type safe accessor

See the Plugin Management section of the Gradle user manual for more information.

The same approach can be used to resolve plugins from composite builds, which do not expose plugin markers yet. Simply map the plugin ID to the corresponding artifact coordinates as shown in the Android samples above.

Configuration avoidance

Gradle 4.9 introduced a new API for creating and configuring tasks in build scripts and plugins. The intent is for this new API to eventually replace the existing API.

One of the major differences between the existing and new Gradle Tasks API is whether or not Gradle spends the time to create Task instances and run configuration code. The new API allows Gradle to delay or completely avoid configuring tasks that will never be executed in a build. For example, when compiling code, Gradle does not need to configure tasks that run tests.

See the Evolving the Gradle API to reduce configuration time blog post and the Task Configuration Avoidance chapter in the user manual for more information.

The Gradle Kotlin DSL embraces the new API by providing DSL constructs to make it easier to use. Rest assured, the whole Gradle API remains available.

If your Groovy build logic is not using the new API, doing the migration to Kotlin and to the new API in distinct steps will make it easier to validate the build behavior.

The following two sections will demonstrate both lazy and eager task creation and configuration.

Configuring tasks

The syntax for configuring tasks is where the Groovy and Kotlin DSLs start to differ significantly. Since Kotlin is a statically typed language, you need to know and provide the type of the task if you want to access any of its type-specific properties and methods.

You also can’t access the task directly as a property of the build script as you can in a Groovy build script. Instead, you query the tasks collection by name and type, as demonstrated in the following sample:

Using the configuration avoidance API & DSL
build.gradle
tasks.named('jar').configure {
    archiveName = 'foo.jar'
}
build.gradle.kts
tasks.named<Jar>("jar") {
    archiveName = "foo.jar"
}
Using the eager API & DSL
build.gradle
jar.archiveName = 'foo.jar'
build.gradle.kts
tasks.getByName<Jar>("jar").archiveName = "foo.jar"
Note that it’s necessary to specify the type of the task explicitly. Otherwise, the script won’t compile because the inferred type will be Task, not Jar, and the archiveName property is specific to the Jar task type.

You can, however, omit the type if you only need to configure properties or call methods that are common to all tasks, i.e. they are declared on the Task interface:

Using the configuration avoidance API & DSL
build.gradle
tasks.named('test').configure {
    doLast {
        println('test completed')
    }
}
build.gradle.kts
tasks.named<Task>("test") {
    doLast {
        println("test completed")
    }
}
Using the eager API & DSL
build.gradle
test.doLast {
    println('test completed')
}
build.gradle.kts
tasks.getByName("test").doLast {
    println("test completed")
}

If you need to configure several properties or call multiple methods on the same task, you can group them in a block as follows:

Using the configuration avoidance API & DSL
build.gradle
tasks.named('jar').configure {
    archiveName = 'foo.jar'
    into('META-INF') {
        from('bar')
    }
}
build.gradle.kts
tasks.named<Jar>("jar") {
    archiveName = "foo.jar"
    into("META-INF") {
        from("bar")
    }
}
Using the eager API & DSL
build.gradle
jar {
    archiveName = 'foo.jar'
    into('META-INF') {
        from('bar')
    }
}
build.gradle.kts
tasks.getByName<Jar>("jar") {
    archiveName = "foo.jar"
    into("META-INF") {
        from("bar")
    }
}

An alternative and more idiomatic way to configure tasks compared to using the tasks collection API is through the use of Kotlin’s delegated properties. This approach is particularly useful if you want to reference the task at a later stage in the build script. The following sample initially configures the jar task’s archiveName property and then later adds an into() declaration via the new delegated property jar:

Using the configuration avoidance API & DSL
build.gradle
def jar = tasks.named('jar')
jar.configure {
    archiveName = 'foo.jar'
}

jar.configure {
    into('META-INF') {
        from('bar')
    }
}
build.gradle.kts
val jar by tasks.existing(Jar::class) {
    archiveName = "foo.jar"
}

jar {
    into("META-INF") {
        from("bar")
    }
}
Using the eager API & DSL
build.gradle
jar {
    archiveName = 'foo.jar'
}

jar.into('META-INF') {
    from('bar')
}
build.gradle.kts
val jar by tasks.getting(Jar::class) {
    archiveName = "foo.jar"
}

jar.into("META-INF") {
    from("bar")
}

Note that even with this approach, you need to provide the type of the task when applying type-specific configuration.

Once the task type is declared, the IDE can assist you in configuring the task with suggestions and by providing access to the task’s source.

Knowing the type of a task

If you don’t know what type a task has, then you can find that information out via the built-in help task. Simply pass it the name of the task you’re interested in using the --task option, like so:

❯ ./gradlew help --task jar
...
Type
     Jar (org.gradle.api.tasks.bundling.Jar)

Let’s bring all this together by running through a quick worked example that configures the bootJar and bootRun tasks of a Spring Boot project. We first determine the types of those tasks via the help task:

❯ ./gradlew help --task bootJar
...
Type
     BootJar (org.springframework.boot.gradle.tasks.bundling.BootJar)
❯ ./gradlew help --task bootRun
...
Type
     BootRun (org.springframework.boot.gradle.tasks.run.BootRun)

Now that we know the types of the two tasks, we can import the relevant types — BootJar and BootRun — and configure the tasks as required. Note that the IDE can assist us with the required imports, so we only need the simple names, i.e. without the full packages. Here’s the resulting build script, complete with imports:

Using the configuration avoidance API & DSL
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.0.2.RELEASE'
}

tasks.named('bootJar').configure {
    archiveName = 'app.jar'
    mainClassName = 'com.example.demo.Demo'
}

tasks.named('bootRun').configure {
    main = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}
build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

plugins {
    java
    id("org.springframework.boot") version "2.0.2.RELEASE"
}

tasks {
    named<BootJar>("bootJar") {
        archiveName = "app.jar"
        mainClassName = "com.example.demo.Demo"
    }

    named<BootRun>("bootRun") {
        main = "com.example.demo.Demo"
        args("--spring.profiles.active=demo")
    }
}
Using the eager API & DSL
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.0.2.RELEASE'
}

bootJar {
    archiveName = 'app.jar'
    mainClassName = 'com.example.demo.Demo'
}

bootRun {
    main = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}
build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

plugins {
    java
    id("org.springframework.boot") version "2.0.2.RELEASE"
}

tasks {
    getByName<BootJar>("bootJar") {
        archiveName = "app.jar"
        mainClassName = "com.example.demo.Demo"
    }

    getByName<BootRun>("bootRun") {
        main = "com.example.demo.Demo"
        args("--spring.profiles.active=demo")
    }
}

Creating tasks

Creating tasks can be done using the script top-level method named task(…​):

Using the eager API & DSL only
build.gradle
task greeting {
    doLast { println 'Hello, World!' }
}
build.gradle.kts
task("greeting") {
    doLast { println("Hello, World!") }
}

Registering or creating tasks can also be done on the tasks container, respectively using the register(…​) and create(…​) methods as shown here:

Using the configuration avoidance API & DSL
build.gradle
tasks.register('greeting') {
    doLast { println("Hello, World!") }
}
build.gradle.kts
tasks.register("greeting") {
    doLast { println("Hello, World!") }
}
Using the eager API & DSL
build.gradle
tasks.create('greeting') {
    doLast { println("Hello, World!") }
}
build.gradle.kts
tasks.create("greeting") {
    doLast { println("Hello, World!") }
}

or by using Kotlin delegated properties for when you need a reference to the created task for later:

Using the configuration avoidance API & DSL
build.gradle
// greeting is of type TaskProvider<Task>
def greeting = tasks.register('greeting') {
    doLast { println("Hello, World!") }
}
build.gradle.kts
// greeting is of type TaskProvider<Task>
val greeting by tasks.registering {
    doLast { println("Hello, World!") }
}
Using the eager API & DSL
build.gradle
// greeting is of type Task
def greeting = tasks.create('greeting') {
    doLast { println("Hello, World!") }
}
build.gradle.kts
// greeting is of type Task
val greeting by tasks.creating {
    doLast { println("Hello, World!") }
}

The samples above create untyped, ad-hoc tasks, but you will more commonly want to create tasks of a specific type. This can also be done using the same create() and creating() methods. Here’s an example that creates a new task of type Zip:

Using the configuration avoidance API & DSL
build.gradle
tasks.create(name: 'docZip', type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
build.gradle.kts
tasks.register<Zip>("docZip") {
    archiveName = "doc.zip"
    from("doc")
}
Using the eager API & DSL
build.gradle
tasks.create(name: 'docZip', type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
build.gradle.kts
tasks.create<Zip>("docZip") {
    archiveName = "doc.zip"
    from("doc")
}

Here’s the same task created using a delegated property:

Using the configuration avoidance API & DSL
build.gradle
// docZip is of type TaskProvider<Zip>
def docZip = tasks.register('docZip', Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
build.gradle.kts
// docZip is of type TaskProvider<Zip>
val docZip by tasks.registering(Zip::class) {
    archiveName = "doc.zip"
    from("doc")
}
Using the eager API & DSL
build.gradle
// docZip is of type Zip
def docZip = tasks.create(name: 'docZip', type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
build.gradle.kts
// docZip is of type Zip
val docZip by tasks.creating(Zip::class) {
    archiveName = "doc.zip"
    from("doc")
}

Configurations and dependencies

Declaring dependencies in existing configurations is similar to the way it’s done in Groovy build scripts, as you can see in this example:

build.gradle
plugins {
    id 'java'
}
dependencies {
    implementation 'com.example:lib:1.1'
    runtimeOnly 'com.example:runtime:1.0'
    testImplementation('com.example:test-support:1.3') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'com.example:test-junit-jupiter-runtime:1.3'
}
build.gradle.kts
plugins {
    java
}
dependencies {
    implementation("com.example:lib:1.1")
    runtimeOnly("com.example:runtime:1.0")
    testImplementation("com.example:test-support:1.3") {
        exclude(module = "junit")
    }
    testRuntimeOnly("com.example:test-junit-jupiter-runtime:1.3")
}

Each configuration contributed by an applied plugin is also available as a member of the configurations container, so you can reference it just like any other configuration.

Knowing what configurations are available

The easiest way to find out what configurations are available is by asking your IDE for suggestions within the configurations container.

You can also use the :kotlinDslAccessorsReport task, which prints the Kotlin code for accessing the configurations contributed by applied plugins and provides the names for all of those accessors.

Note that if you do not use the plugins {} block to apply your plugins, then you won’t be able to configure the dependency configurations provided by those plugins in the usual way. Instead, you will have to use string literals for the configuration names, which means you won’t get IDE support:

build.gradle
apply plugin: 'java'
dependencies {
    implementation 'com.example:lib:1.1'
    runtimeOnly 'com.example:runtime:1.0'
    testImplementation('com.example:test-support:1.3') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'com.example:test-junit-jupiter-runtime:1.3'
}
build.gradle.kts
apply(plugin = "java")
dependencies {
    "implementation"("com.example:lib:1.1")
    "runtimeOnly"("com.example:runtime:1.0")
    "testImplementation"("com.example:test-support:1.3") {
        exclude(module = "junit")
    }
    "testRuntimeOnly"("com.example:test-junit-jupiter-runtime:1.3")
}

or you can bring the configurations into scope thanks to Kotlin’s delegated properties:

build.gradle
apply plugin: 'java'
dependencies {
    implementation 'com.example:lib:1.1'
    runtimeOnly 'com.example:runtime:1.0'
    testImplementation('com.example:test-support:1.3') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'com.example:test-junit-jupiter-runtime:1.3'
}
build.gradle.kts
apply(plugin = "java")
val implementation by configurations
val runtimeOnly by configurations
val testImplementation by configurations
val testRuntimeOnly by configurations
dependencies {
    implementation("com.example:lib:1.1")
    runtimeOnly("com.example:runtime:1.0")
    testImplementation("com.example:test-support:1.3") {
        exclude(module = "junit")
    }
    testRuntimeOnly("com.example:test-junit-jupiter-runtime:1.3")
}

This is just one more reason to use the plugins {} block whenever you can!

Custom configurations and dependencies

Sometimes you need to create your own configurations and attach dependencies to them. The following example declares two new configurations:

  • db, to which we add a PostgreSQL dependency

  • integTestImplementation, which is configured to extend the testImplementation configuration and to which we add a different dependency

build.gradle
configurations {
    db
    integTestImplementation {
        extendsFrom testImplementation
    }
}

dependencies {
    db 'org.postgresql:postgresql'
    integTestImplementation 'com.example:integ-test-support:1.3'
}
build.gradle.kts
val db by configurations.creating
val integTestImplementation by configurations.creating {
    extendsFrom(configurations["testImplementation"])
}

dependencies {
    db("org.postgresql:postgresql")
    integTestImplementation("com.example:integ-test-support:1.3")
}

Note that we can only use the db(…​) and integTestImplementation(…​) notation within the dependencies {} block in the above example because both configurations are declared as delegated properties beforehand via the creating() method. If the configurations were defined elsewhere, you could only reference them either by first creating delegating properties via configurations — as opposed to configurations.creating() — or by using string literals within the dependencies {} block. The following example demonstrates both approaches:

build.gradle.kts
// get the existing 'testRuntimeOnly' configuration
val testRuntimeOnly by configurations

dependencies {
    testRuntimeOnly("com.example:test-junit-jupiter-runtime:1.3")
    "db"("org.postgresql:postgresql")
    "integTestImplementation"("com.example:integ-test-support:1.3")
}

Multi-project builds

The model for multi-project builds is the same regardless of whether the projects within them have Groovy DSL or Kotlin DSL build scripts. Multi-project builds can even have a mix of the two.

As you can see from the following settings file and build script, the Kotlin syntax is very similar to the Groovy one:

settings.gradle
include 'core', 'cli'
build.gradle
subprojects {
    apply plugin: 'java'
}
project(':cli') {
    apply plugin: 'application'
}
settings.gradle.kts
include("core", "cli")
build.gradle.kts
subprojects {
    apply(plugin = "java")
}
project(":cli") {
    apply(plugin = "application")
}

However there are some subtleties that can make your life easier or complicate things with the Kotlin DSL.

In multi project builds, projects get configured top to bottom according to the project hierarchy. In other words, project build scripts are evaluated top to bottom according to the project hierarchy.

This makes it possible for the Kotlin DSL to make Kotlin extensions to configure plugins applied by a parent project available in a sub project build script, just as if you used the plugins {} block directly in the sub project build script.

Following up on our example multi project build, the :cli build script would look as follows:

cli/build.gradle
mainClassName = "com.example.CliMain"
cli/build.gradle.kts
application {
    mainClassName = "com.example.CliMain"
}
Configuration on demand

Enabling the incubating configuration on demand feature is not recommended as it can lead to very hard-to-diagnose problems. The strictness of the Gradle Kotlin DSL makes it even more likely that problems will arise. In order to save you trouble, you should disable it if it’s already enabled.

The same is true for evaluationDependsOn(projectPath) and evaluationDependsOnChildren(). Favor decoupling your projects instead.

Migration strategies

As we’ve seen above, both scripts using the Kotlin DSL and those using the Groovy DSL can participate in the same build. In addition, Gradle plugins from the buildSrc directory, an included build or an external location can be implemented using any JVM language. This makes it possible to migrate a build progressively, piece by piece, without blocking your team.

Two approaches to migrations stand out:

  • Migrating the existing syntax of your build to Kotlin, bit by bit, while retaining the structure — what we call a mechanical migration

  • Restructuring your build logic towards Gradle best practices and switching to Kotlin DSL as part of that effort

Both approaches are viable. A mechanical migration will be enough for simple builds. A complex and highly dynamic build may require some restructuring anyway, so in such cases reimplementing build logic to follow Gradle best practice makes sense.

Since applying Gradle best practices will make your builds easier to use and faster, we recommend that you migrate all projects in that way eventually, but it makes sense to focus on the projects that have to be restructured first and those that would benefit most from the improvements.

Also consider that the more parts of your build logic rely on the dynamic aspects of Groovy, the harder they will be to use from the Kotlin DSL. You’ll find recipes on how to cross the dynamic boundaries from static Kotlin in the Interoperability section below, regardless of where the dynamic Groovy build logic resides.

There are two key best practices that make it easier to work within the static context of the Kotlin DSL:

  • Using the plugins {} block

  • Putting local build logic in the build’s buildSrc directory

The plugins {} block — explained in the Gradle User Manual — is about keeping your build scripts declarative in order to get the best out of the Kotlin DSL. You saw above how to apply this practice even with external plugins published without the required metadata.

Utilizing the buildSrc project — also explained in the Gradle User Manual — is about organizing your build logic into shared local plugins and conventions that are easily testable and provide good IDE support.

Kotlin DSL build structure samples

Depending on your build structure you might be interested in the following samples built with the Kotlin DSL:

See the samples README for general instructions.

Interoperability

When mixing languages in your build logic, you may have to cross language boundaries. An extreme example would be a build that uses tasks and plugins that are implemented in Java, Groovy and Kotlin, while also using both Kotlin DSL and Groovy DSL build scripts.

Quoting the Kotlin reference documentation:

Kotlin is designed with Java Interoperability in mind. Existing Java code can be called from Kotlin in a natural way, and Kotlin code can be used from Java rather smoothly as well.

Both calling Java from Kotlin and calling Kotlin from Java are very well covered in the Kotlin reference documentation.

The same mostly applies to interoperability with Groovy code. In addition, the Kotlin DSL provides several ways to opt into Groovy semantics, which we look at next.

Static extensions

Both the Groovy and Kotlin languages support extending existing classes via Groovy Extension modules and Kotlin extensions.

To call a Kotlin extension function from Groovy, call it as a static function, passing the receiver as the first parameter:

build.gradle
TheTargetTypeKt.kotlinExtensionFunction(receiver, "parameters", 42, aReference)

Kotlin extension functions are package-level functions and you can learn how to locate the name of the type declaring a given Kotlin extension in the Package-Level Functions section of the Kotlin reference documentation.

To call a Groovy extension method from Kotlin, the same approach applies: call it as a static function passing the receiver as the first parameter. Here’s an example:

build.gradle.kts
TheTargetTypeGroovyExtension.groovyExtensionMethod(receiver, "parameters", 42, aReference)

Named parameters and default arguments

Both the Groovy and Kotlin languages support named function parameters and default arguments, although they are implemented very differently. Kotlin has fully-fledged support for both, as described in the Kotlin language reference under named arguments and default arguments. Groovy implements named arguments in a non-type-safe way based on a Map<String, ?> parameter, which means they cannot be combined with default arguments. In other words, you can only use one or the other in Groovy for any given method.

Calling Kotlin from Groovy

To call a Kotlin function that has named arguments from Groovy, just use a normal method call with positional parameters. There is no way to provide values by argument name.

To call a Kotlin function that has default arguments from Groovy, always pass values for all the function parameters.

Calling Groovy from Kotlin

To call a Groovy function with named arguments from Kotlin, you need to pass a Map<String, ?>, as shown in this example:

build.gradle.kts
groovyNamedArgumentTakingMethod(mapOf(
    "parameterName" to "value",
    "other" to 42,
    "and" to aReference))

To call a Groovy function with default arguments from Kotlin, always pass values for all the parameters.

Groovy closures from Kotlin

You may sometimes have to call Groovy methods that take Closure arguments from Kotlin code. For example, some third-party plugins written in Groovy expect closure arguments.

Gradle plugins written in any language should prefer the type Action<T> in place of closures. Groovy closures and Kotlin lambdas are automatically mapped to arguments of that type.

In order to provide a way to construct closures while preserving Kotlin’s strong typing, two helper methods exist:

  • closureOf<T> {}

  • delegateClosureOf<T> {}

Both methods are useful in different circumstances and depend upon the method you are passing the Closure instance into.

Some plugins expect simple closures, as with the Bintray plugin:

build.gradle.kts
bintray {
    pkg(closureOf<PackageConfig> {
        // Config for the package here
    })
}

In other cases, like with the Gretty Plugin when configuring farms, the plugin expects a delegate closure:

build.gradle.kts
farms {
    farm("OldCoreWar", delegateClosureOf<FarmExtension> {
        // Config for the war here
    })
}

There sometimes isn’t a good way to tell, from looking at the source code, which version to use. Usually, if you get a NullPointerException with closureOf<T> {}, using delegateClosureOf<T> {} will resolve the problem.

Also see the groovy-interop sample.

The Kotlin DSL Groovy Builder

If some plugin makes heavy use of Groovy metaprogramming, then using it from Kotlin or Java or any statically-compiled language can be very cumbersome.

The Kotlin DSL provides a withGroovyBuilder {} utility extension that attaches the Groovy metaprogramming semantics to objects of type Any. The following example demonstrates several features of the method on the object target:

build.gradle.kts
target.withGroovyBuilder {                                          (1)

    // GroovyObject methods available                               (2)
    val foo = getProperty("foo")
    setProperty("foo", "bar")
    invokeMethod("name", arrayOf("parameters", 42, aReference))

    // Kotlin DSL utilities
    "name"("parameters", 42, aReference)                            (3)
        "blockName" {                                               (4)
            // Same Groovy Builder semantics on `blockName`
        }
    "another"("name" to "example", "url" to "https://example.com/") (5)
}
1 The receiver is a GroovyObject and provides Kotlin helpers
2 The GroovyObject API is available
3 Invoke the methodName method, passing some parameters
4 Configure the blockName property, maps to a Closure taking method invocation
5 Invoke another method taking named arguments, maps to a Groovy named arguments Map<String, ?> taking method invocation

The maven-plugin sample demonstrates the use of the withGroovyBuilder() utility extensions for configuring the uploadArchives task to deploy to a Maven repository with a custom POM using Gradle’s core Maven Plugin. Note that the recommended Maven Publish Plugin provides a type-safe and Kotlin-friendly DSL that allows you to easily do the same and more without resorting to withGroovyBuilder().

Using a Groovy script

Another option when dealing with problematic plugins that assume a Groovy DSL build script is to configure them in a Groovy DSL build script that is applied from the main Kotlin DSL build script:

build.gradle.kts
plugins {
    id("dynamic-groovy-plugin") version "1.0"               (1)
}
apply(from = "dynamic-groovy-plugin-configuration.gradle")  (2)
dynamic-groovy-plugin-configuration.gradle
native {                                                    (3)
    dynamic {
        groovy as Usual
    }
}
1 The Kotlin build script requests and applies the plugin
2 The Kotlin build script applies the Groovy script
3 The Groovy script uses dynamic Groovy to configure plugin

Summary

In this guide you had a tour of the main differences between Gradle’s Groovy DSL and Kotlin DSL by comparing build scripts doing common things, while being introduced to the main idioms of the Kotlin DSL. You also took a look at possible migration strategies in the light of the structure of builds. Last, but not least, you learnt how the two DSLs inter-operate.

Next steps

  • The Gradle Best Practices user manual chapters contain reference documentation on how to structure your builds.

  • The kotlin-dsl samples contain examples of various build structures using the Kotlin DSL.

Help improve this guide

Have feedback or a question? Found a typo? Like all Gradle guides, help is just a GitHub issue away. Please add an issue or pull request to gradle-guides/migrating-build-logic-from-groovy-to-kotlin and we’ll get back to you.