Skip to content
Book Cover of Extending Android Builds

2-3: Exploring Kotlin and Gradle Kotlin DSL ​

In the preceding section, we discovered how complex Plugins can be authored in buildSrc or using Composite Builds. This naturally leads us to ponder the programming language and script type that would be most suitable for these endeavors. What potential benefits and drawbacks are linked to the Kotlin DSL script? Why does this book advocate the use of Kotlin and KTS for script and Plugin development?

KTS represents a script file grounded in Kotlin Script. Within the framework of this book, we frequently refer to the *.gradle.kts script devised by Gradle on the basis of the Kotlin DSL. It fulfills a function akin to *.gradle files (Gradle Groovy DSL). We will not delve into the Kotlin basic knowledge in this segment. Earlier in Chapter 1, we touched upon the benefits of migrating to Kotlin DSL. This section is dedicated to exploring the various applications of Kotlin and KTS in tools, along with techniques and architectural changes when transitioning from Gradle Groovy DSL (abbreviated henceforth as GGD) to Gradle Kotlin DSL (abbreviated henceforth as GKD).

The source code used in this section is located in the groovy-to-kotlin-dsl, common-kts-script, and slack directories.

2-3-1: Crafting Tools Using Kotlin ​

The Android ecosystem and toolchain have, over the past few years, been significantly enhanced or shifted to accommodate Kotlin, as evidenced by:

  • The advent of numerous -ktx toolkits in Jetpack, which seamlessly integrate with Kotlin-based projects, to boost development efficiency.
  • AGP and an array of build tools (including D8/R8) employing Kotlin for the development of new features. The related Gradle Plugins have shifted from Groovy DSL to Kotlin DSL, with older code gradually being transitioned.
  • Kotlin's advanced capabilities, such as DSL, have been verified for longer periods by a number of high-profile projects, including Ktor, Koin, and Anko.

Kotlin is getting more and more attention as a viable solution. A typical problem is that knowledge is frequently lost due to its lack of direct application to the development of main application features. A feasible solution to this problem might be to begin developing independent SDKs or Gradle Plugins using Kotlin. This not only improves productivity and provides opportunities to hone new abilities, but it also has no negative impact on the overall project's architecture.

Kotlin's low barrier to entry and thriving ecosystem make it an appealing option for porting Gradle build scripts to the language's domain-specific syntax (DSL). Drawing a parallel with the Javascript development ecosystem, frontend developers find pleasure in employing JS to design various peripheral tools, including build tools and build scripts. Even mobile developers would be pleased if a single language could be used for a wide variety of projects and tools.

2-3-2: Generic KTS Scripts ​

At this juncture, a prevailing misconception about KTS is that it exclusively refers to the Gradle script with Kotlin DSL. In reality, *.kts is a universal Kotlin script, with various potential usages. Comparable tools include Shell and Python scripts. The merits of KTS scripts lie in their uniformity of App development and ease of use, coupled with the ability to reference external Maven dependencies within the script.

However, it does have a limitation in that it necessitates the installation of Kotlin dependencies on the system, which are not typically pre-installed.

There are two primary standards for KTS at present:

  • KScript standard: The file extension *.kts was established by a third party in 2016 (github.com/holgerbrandl/kscript). It was demonstrated in detail at KotlinConf 2017 and remains the most functional standard to date, covering script caching, external dependency references, command-line interpreters, automatic generation of Gradle projects (for easier IDE integration), etc. The KScript tool needs to be installed in addition to the Kotlin package.
  • Kotlin's official standard: The file extension *.main.kts was introduced with Kotlin 1.3 (github.com/Kotlin/KEEP ). It completed some of the main functions of KScript in version 1.4 and eliminated the need for a Gradle project to directly support dependency references from Intellij IDEA (as well as Android Studio).
Kotlin
@file:DependsOn("com.squareup.okhttp3:okhttp:3.8.1")

import okhttp3.*
import java.io.IOException

OkHttpClient().newCall(Request.Builder()
  .url("http://publicobject.com/helloworld.txt")
  .build())
  .enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
      println(e.message)
    }
    override fun onResponse(call: Call, resp: Response) {
      println(resp.body()!!.string())
    }
  })

The simple test.main.kts script above reads an online helloworld.txt and displays it on the console (Figure 2.3.1).

Figure 2.3.1: There is an Android icon in the file

2-3-3: The Gradle Kotlin DSL ​

Gradle Kotlin DSL is premised on the general-purpose use of KTS scripts. It further specifies the context (such as Project and Settings objects, refer to Section 2-2) of the script and introduces additional DSLs to simplify configuration.

The transition from Groovy DSL to Kotlin DSL is not difficult.

  • Learn the fundamentals of Kotlin's DSL.
  • Follow official migration guide for Gradle and the Android Gradle Plugin while taking a look at some examples (such as the sample project Playground of this book).

I would like to introduce a useful tool here, which is analogous to the built-in Java to Kotlin conversion tool in the IDE, the GradleKotlinConverter (https://github.com/bernaferrari/GradleKotlinConverter). It allows for the one-click conversion of simple and frequently used syntax from GGD to GKD. Occasionally, it might encounter minor errors while converting some new configurations. At this point, we can manually modify it according to the configuration of Kotlin DSL in the AGP document. It is noteworthy that this tool is based on a generic KTS script itself.

Bash
$ ./gradlekotlinconverter.kts ./Playground/groovy-to-kotlin-dsl\
  /app/build.gradle
...
[17:03:54] - Trying to open file.. Success!
[17:03:54] -- Starting conversion.. Success!
[17:03:54] --- Saving to: "/Users/2bab/Desktop/Playground/groovy-to-\
  kotlin-dsl/app/build.gradle.kts".. Success!

Clone the tool repository to your local machine and execute the gradlekotlinconverter.kts script for conversion. To begin, let's examine the build.gradle script prior to conversion:

Groovy
plugins {
  id 'com.android.application'
  id 'kotlin-android'
}

android {
  compileSdk 30
  buildToolsVersion "30.0.3"

  defaultConfig {
    applicationId "me.xx2bab.buildinaction.dslconversion"
    minSdk 21
    targetSdk 30
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "..."
  }

  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('...'), 'proguard-rules.pro'
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
  kotlinOptions {
    jvmTarget = '1.8'
  }
}

dependencies {
  implementation 'androidx.core:core-ktx:1.3.2'
  ...
}

Upon conversion with minor adjustments, the resulting build.gradle.kts script becomes:

Kotlin
plugins {
  id("com.android.application")
  id("kotlin-android")
}

android {
  compileSdk = 30
  buildToolsVersion = "30.0.3"

  defaultConfig {
    applicationId = "me.xx2bab.buildinaction.dslconversion"
    minSdk = 21
    targetSdk = 30
    versionCode = 1
    versionName = "1.0"

    testInstrumentationRunner = "..."
  }

  buildTypes {
    named("release") {
      isMinifyEnabled = false
      setProguardFiles(listOf(getDefaultProguardFile("..."),
        "proguard-rules.pro"))
    }
  }
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
  }
  kotlinOptions {
    jvmTarget = "1.8"
  }
}

dependencies {
  implementation("androidx.core:core-ktx:1.3.2")
  ...
}

In basic usage, there is minimal divergence between the two DSLs. Let's delve further into the rationale behind migrating to GKD. The primary purpose of DSL is to facilitate more succinct code-writing within a specific domain. However, to accomplish this, one may have to simplify a little bit more language specifications. For instance, certain method calls do not require brackets according to Groovy DSL rules:

Groovy
// compileSdkVersion(30)
compileSdkVersion 30

The latter style is not often used in most programming languages. This specific rule necessitates comprehension and practice by beginners for effective use. Let's examine a more illustrative example that highlights the challenges associated with the readability of Groovy DSL. As Android developers, we are all familiar with how to configure a build type (Groovy DSL):

Groovy
buildTypes {
  debug {
    minifyEnabled false
  }
  release {
    minifyEnabled false
  }
  staging {
    minifyEnabled false
  }
}

The usage in the context of Gradle Kotlin DSL is as follows:

Kotlin
buildTypes {
  getByName("debug") {
    isMinifyEnabled = false
  }
  getByName("release") {
    isMinifyEnabled = false
  }
  create("staging") {
    isMinifyEnabled = false
  }
//  register("staging") {
//    isMinifyEnabled = false
//  }
}

The Android Gradle Plugin (AGP) provides pre-configured debug and release build types. However, the staging type requires creation through the create(...) or register(...) functions. This API style also applies when creating Gradle Tasks, with the distinction lying between immediate creation and on-demand creation.

  • GGD leverages an influential metaprogramming protocol, allowing for a more flexible DSL: within the buildTypes scope, NamedDomainObjectContainer is implemented. It does not encompass an implementation method called debug or release, but Groovy can facilitate a call to a non-existent method, for instance, through methodMissing(), thereby dynamically obtaining or creating a build type.
  • Although GKD might lack some flexibility in expression, it offers clearer insights into the underlying logic to the user: debug and release are pre-set, hence we retrieve and modify them; staging is custom-created.
  • Indeed, the same writing style in GKD is also valid in GGD.

For a more comprehensive understanding of NamedDomainObjectContainer, please refer to Section 4-3.

GKD enhances completion and readability significantly, partly because of the absence of Groovy's dynamic typing and metaprogramming features, such as method synthesis and interception calls at runtime for non-existent methods, which contribute to its succinct syntax. Conversely, to offer a configuration that aligns with GGD, GKD introduces Type-Safe Model Accessors, where Model refers to the configuration model within the DSL. For instance, with GKD, Gradle generates the following code for android(...) as an Extension entry point of the AGP:

Kotlin
// Accessors377twfxlhpj2n65rquy9ybeqs.kt
/**
 * Retrieves the [android][com.android.build.gradle.internal
 *   .dsl.BaseAppModuleExtension] extension.
 */
val Project.`android`: BaseAppModuleExtension get() =
  (this as ExtensionAware).extensions.getByName("android")
  as BaseAppModuleExtension

/**
 * Configures the [android][com.android.build.gradle.internal.dsl
 *   .BaseAppModuleExtension] extension.
 */
fun Project.`android`(config: Action<BaseAppModuleExtension>): Unit =
  (this as ExtensionAware).extensions.configure("android", config)

android{...} is an Kotlin extension method based on the Project object, android(configure: Action<BaseAppModuleExtension>)(...), which is a wrapper for the raw Gradle API configure(...). These extensions are immediately generated after Gradle parses the plugins{} block, while Plugins referred through the Project#apply(...) method do not support above mechanism (you have to fallback to configure<BaseAppModuleExtension>(...)).

Moreover, the GKD has superior IDE completion support. For instance, within the android{} block, you can view the completion dialog when typing the partial function name of compileSdkVersion, and even navigate to the corresponding source code after completing the editing (Figure 2.3.2 and 2.3.3).

Figure 2.3.2: GKD's completion dropdown for "compileSdkV"

Figure 2.3.3: GKD's source code navigation

However, GGD's assistance in this area is not as robust, and you will likely see the error message "Cannot find the declaration to go to" if you try to navigate to source (Figure 2.3.4).

Figure 2.3.4: "Cannot find declaration to go to" error with GGD

Despite its advantages, GKD also presents challenges. Earlier versions of GKD encountered numerous issues, including:

  • Performance concerns with editing and completion: During 2018-2019, editing script files exhibited considerable lag in an IDE and could only be used via a text editor.
  • Performance problems with Gradle Sync: In 2018, the average evaluation time across multiple tests was around 3 times longer than that of Groovy DSL.

These have been addressed thanks to joint efforts from JetBrains and Gradle. Recent tests with new versions of the tools have demonstrated swift response times for editing and completion.

Further, GKD experienced significant acceleration from 2019, reducing the synchronization time compared to GGD. As of early 2021, even though GKD is still behind GGD in initial opening times, some tests show parity and even surpass GGD. This includes optimizations exclusive to GKD, like Gradle 6.8's compilation avoidance, which specifies that scripts and Plugins in /buildSrc that don't alter the external interface will not trigger global recompilation.

Gradle will continue to develop and maintain both GKD and GGD, but the default script DSL has already shifted to Gradle Kotlin DSL in April 2023, with Kotlin recommended for writing complex Gradle Pugins as well. Currently, the choice of GKD is about leveraging IDE collaboration and readability. As for performance, we eagerly await subsequent optimizations from the Kotlin compiler and Gradle toolchain, and recommend that users upgrade their tools to the latest version when migrating to GKD.

2-3-4: Type-Safe Model Accessor Lookup ​

For applying GKD on Android, you can effectively get started by following the official migration tutorials, the demo of the Github Android repository, and using the migration tools mentioned above. However, when dealing with unfamiliar Extensions or third-party Plugin configurations, here is a trick to look up the APIs:

Bash
./gradlew kotlinDslAccessorsReport > dsl.txt

This command collects and generates Type-Safe Model Accessors (TSMA) extensions for all currently used types (Figure 2.3.5). By searching through this large file, we can rapidly locate the needed extension usage. For instance, if we are unsure about how to configure the AGP Plugin, we find that an android(...) extension method is mounted on the Gradle Project object, thus we can use android {} for configuration in the script. It also reveal to us that the implementation class is BaseAppModuleExtension, allowing further examination of the extension source code.

Figure 2.3.5: TSMA for "android" Extension

2-3-5: Script Plugin Organizational Strategy ​

With GGD scripting, we found a common practice to extract individual configurations from the *.gradle Script Plugins. For instance, maven-release.gradle and jacoco.gradle, which may extract the Maven release and Jacoco coverage test features respectively. This extraction accomplished the separation of concerns and improved the reusability of these scripts.

However, we ran into compile-time issues when switching to GKD scripts *.gradle.kts via the same pattern. A common problem was that the scripts couldn't reference the present Plugins APIs or alter their settings. This issue is most evident when one attempts to find the android{...} block unrecognized within the stand-alone GKD script.

With GKD, blocks such as android{...} extensions are enabled by generating TSMA, but Independent Script Plugins concept do not support the generation of this "helper" code. The suggested remedy by Gradle is straightforward: migrate the scripts with Plugin references to buildSrc (or another Composited Build Plugin module) and therefore transform them into Precompiled Script Plugins.

Taking the plugin-config module in the slack sample project as an example, which houses multiple configuration scripts referencing external Plugins.

├── slack
│   └── app
│     └── build.gradle.kts
│   └── build-env
│   └── plugin-config *
│     └── src
│       └── main
│         └── kotlin
│           ├── slack-config.gradle.kts
│           ├── slack-lazy-config.gradle.kts
│           └── ...
│     ├── build.gradle.kts
│     └── settings.gradle.kts
│   ├── plugins
│   ├── build.gradle.kts
│   └── settings.gradle.kts

By incorporating *.gradle.kts Script Plugins into plugin-config, Kotlin scripts get precompiled before joining the compile classpath. At the same time, existing Plugins such as AGP or Slack notification Plugins can generate Type-Safe Model Accessor within the scope of plugin-config module once we add them to the dependencies. Thus, slack-config.gradle.kts is able to configure the extension of Slack notification Plugin.

Finally, if you still prefer using an independent Script Plugin within GKD:

  • Writing independent Tasks usually does not cause any issues.
  • The issue of external dependency references can be resolved by separately adding a buildscript{} block within this Script Plugin, but it does not come with the TSMA.
  • The APIs generated by the TSMA feature can be manually simulated by invoking the raw Gradle API, such as ExtensionContainer#configure(...).

2-3-6: In Summary ​

In this section, we've discussed several aspects of the rationale behind migrating to Kotlin and Gradle Kotlin DSL scripts. We've objectively analyzed the advantages and disadvantages of Gradle Kotlin DSL practical cases and engaged in thoughtful extrapolation. As development tools and environments continue to be refined through various efforts, it's fair to say that a migration to Gradle Kotlin DSL (and Plugin development with Kotlin) is a beneficial choice.