This section 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

Please also read the Gradle Kotlin DSL Primer to learn the specificities, limitations and usage of the Gradle Kotlin DSL.

The rest of the user manual contain build script excerpts that demonstrate both the Groovy DSL and the Kotlin DSL. This is the best place where to find how to do this and what with each DSL ; and it covers all Gradle features from using plugins to customizing the dependency resolution behavior.

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. Builds with slow configuration time might affect the IDE responsiveness, please check out the documentation on Gradle performance.

  • 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.

  • Enabling the incubating configuration on demand feature is not recommended as it can lead to very hard-to-diagnose problems.

Read more in the Gradle Kotlin DSL Primer.

If you run to trouble or a suspected bug, please take advantage of the gradle/gradle 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.

Prepare your Groovy scripts

Some simple Kotlin and Groovy language differences can make converting scripts tedious:

  • Groovy strings can be quoted with single quotes 'string' or double quotes "string" whereas Kotlin requires double quotes "string".

  • Groovy allows to omit parentheses when invoking functions whereas Kotlin always requires the parentheses.

  • The Gradle Groovy DSL allows to omit the = assignment operator when assigning properties whereas Kotlin always requires the assignment operator.

As a first migration step, it is recommended to prepare your Groovy build scripts by

  • unifying quotes using double quotes,

  • disambiguating function invocations and property assignments (using respectively parentheses and assignment operator).

The former can easily be done by searching for ' and replacing by ". For example,

group = 'com.acme'
dependencies {
    implementation 'com.acme:example:1.0'
}

becomes:

group "com.acme"
dependencies {
    implementation "com.acme:example:1.0"
}

The next step is a bit more involved as it may not be trivial to distinguish function invocations and property assignments in a Groovy script. A good strategy is to make all ambiguous statements property assignments first and then fix the build by turning the failing ones to function invocations.

For example,

group "com.acme"
dependencies {
    implementation "com.acme:example:1.0"
}

becomes:

group = "com.acme"                          (1)
dependencies {
    implementation("com.acme:example:1.0")  (2)
}
1 Property assignment
2 Function invocation

While staying valid Groovy, it is now unambiguous and close to the Kotlin syntax, making it easier to then rename the script to turn it into a Gradle Kotlin DSL script.

It is important to note that while Groovy extra properties can be modified using an object’s ext property, in Kotlin they are modified using the extra property. It is important to look at each object and update the build scripts accordingly.

You can find an example in the userguide.

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:

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

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

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.kts
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.8")
    }
}

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

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.
See the plugins {} block documentation in the Gradle user manual for more information.

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.kts
plugins {
    jacoco
}

jacoco {
    toolVersion = "0.8.1"
}
build.gradle
plugins {
    id '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.kts
apply(plugin = "checkstyle")

configure<CheckstyleExtension> {
    maxErrors = 10
}
build.gradle
apply plugin: "checkstyle"

checkstyle {
    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 of the Gradle Kotlin DSL documentation 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 below.

Keeping build scripts declarative

To get the most benefits of the Gradle Kotlin DSL you should strive to keep your build scripts declarative. The main thing to remember here is that in order to get type-safe accessors, plugins must be applied before the body of build scripts.

It is strongly recommended to read about configuring plugins with the Gradle Kotlin DSL in the Gradle user manual.

If your build is a multi-project build, like mostly all Android builds for example, please also read the subsequent section about multi-project builds.

Finally, there are strategies to use the plugins {} block with plugins that aren’t published with the correct metadata, such as the Android Gradle Plugin.

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 configuration avoidance by making the type-safe model accessors leverage the new APIs and providing DSL constructs to make them easier to use. Rest assured, the whole Gradle API remains available.

Configuring tasks

The syntax for configuring tasks is where the Groovy and Kotlin DSLs start to differ significantly.

build.gradle.kts
tasks.jar {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.jar {
    archiveFileName = 'foo.jar'
}

Note that in Kotlin the tasks.jar {} notation leverage the configuration avoidance API and defer the configuration of the jar task.

If the type-safe task accessor tasks.jar isn’t available, see the configuring plugins section above, you can fallback to using the tasks container API. The Kotlin flavor of the following sample is strictly equivalent to the one using the type-safe accessor above:

build.gradle.kts
tasks.named<Jar>("jar") {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.named('jar') {
    archiveFileName = 'foo.jar'
}

Note that since Kotlin is a statically typed language, it is necessary to specify the type of the task explicitly. Otherwise, the script will not compile because the inferred type will be Task, not Jar, and the archiveName property is specific to the Jar task type.

If configuration avoidance is getting in your way migrating and you want to eagerly configure a task just like Groovy you can do so by using the eager configuration API on the tasks container:

build.gradle.kts
tasks.getByName<Jar>("jar") {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.getByName('jar') {
    archiveFileName = 'foo.jar'
}

Working with containers in the Gradle Kotlin DSL is documented in detail here.

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:

build.gradle.kts
plugins {
    java
    id("org.springframework.boot") version "2.7.8"
}

tasks.bootJar {
    archiveFileName = "app.jar"
    mainClass = "com.example.demo.Demo"
}

tasks.bootRun {
    mainClass = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.8'
}

tasks.bootJar {
    archiveFileName = 'app.jar'
    mainClass = 'com.example.demo.Demo'
}

tasks.bootRun {
    mainClass = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}

This is pretty self explanatory. The main difference is that the task configuration automatically becomes lazy when using the Kotlin DSL accessors.

Now, for the sake of the example, let’s look at the same configuration applied using the API instead of the type-safe accessors that may not be available depending on the build logic structure, see the corresponding documentation in the Gradle user manual for more information.

We first determine the types of the bootJar and bootRun 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:

build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

// TODO:Finalize Upload Removal - Issue #21439
plugins {
    java
    id("org.springframework.boot") version "2.7.8"
}

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

tasks.named<BootRun>("bootRun") {
    mainClass = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.8'
}

tasks.named('bootJar') {
    archiveFileName = 'app.jar'
    mainClass = 'com.example.demo.Demo'
}

tasks.named('bootRun') {
    mainClass = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}

Creating tasks

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

build.gradle.kts
task("greeting") {
    doLast { println("Hello, World!") }
}
build.gradle
task greeting {
    doLast { println 'Hello, World!' }
}

Note that the above eagerly configures the created task with both Groovy and Kotlin DSLs.

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

build.gradle.kts
tasks.register("greeting") {
    doLast { println("Hello, World!") }
}
build.gradle
tasks.register('greeting') {
    doLast { println('Hello, World!') }
}
build.gradle.kts
tasks.create("greeting") {
    doLast { println("Hello, World!") }
}
build.gradle
tasks.create('greeting') {
    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 register() and create() methods. Here’s an example that creates a new task of type Zip:

build.gradle.kts
tasks.register<Zip>("docZip") {
    archiveFileName = "doc.zip"
    from("doc")
}
build.gradle
tasks.register('docZip', Zip) {
    archiveFileName = 'doc.zip'
    from 'doc'
}
build.gradle.kts
tasks.create<Zip>("docZip") {
    archiveFileName = "doc.zip"
    from("doc")
}
build.gradle
tasks.create(name: 'docZip', type: Zip) {
    archiveFileName = '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.kts
plugins {
    `java-library`
}
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
plugins {
    id 'java-library'
}
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.kts
apply(plugin = "java-library")
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
apply plugin: 'java-library'
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.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")
}
build.gradle
configurations {
    db
    integTestImplementation {
        extendsFrom 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")
}

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 of the Gradle Kotlin DSL documentation, 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 is about keeping your build scripts declarative in order to get the best out of the Kotlin DSL.

Utilizing the buildSrc project 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 user manual chapters:

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.

On the Gradle Kotlin DSL and interoperability

Please find detailed documentation in the interoperability section of the Gradle Kotlin DSL Primer.