Gradle Kotlin DSL

Every Android project you've built across this track has been described, underneath the IDE, by a Gradle build script — the file that lists dependencies, sets the compile SDK, and defines how a debug build differs from a release one. This lesson looks directly at that file: the modern Kotlin DSL that writes it in, what it replaced, and a full worked build.gradle.kts for TrailLog covering everything from earlier lessons in this track.

Kotlin DSL vs. the Older Groovy DSL

Gradle build scripts can be written in two languages: the original Groovy DSL (files named build.gradle) or the newer Kotlin DSL (files named build.gradle.kts). Android Studio has defaulted every new project to the Kotlin DSL for several years now, and the reason comes down to tooling rather than the language itself: a .kts file is genuinely just Kotlin, so Android Studio's Kotlin support — real autocomplete, jump-to-definition on a Gradle API, and compile-time type checking that catches a typo like a misspelled configuration name before you even run a build — applies to it the same way it applies to any other Kotlin file in the project. A Groovy build script gets much weaker IDE support in comparison, because Groovy is dynamically typed and IntelliJ's tooling for it was never as deep as its Kotlin tooling.

  • Groovy DSL (build.gradle) — the original, still widely seen in older tutorials and existing projects; loose syntax, weaker autocomplete, errors often only surface at build time.
  • Kotlin DSL (build.gradle.kts) — the current default for new projects; real static typing means many mistakes are flagged directly in the editor, before you ever hit "Sync" or "Build."

A Full Annotated build.gradle.kts for TrailLog

Here's a realistic app/build.gradle.kts pulling together plugins and libraries from across this track — Room, Retrofit, Hilt, and kotlinx.serialization:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.serialization")
    id("com.google.dagger.hilt.android")
    id("kotlin-kapt")
}

android {
    namespace = "com.traillog.app"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.traillog.app"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
        debug {
            applicationIdSuffix = ".debug"
        }
    }
}

dependencies {
    implementation("androidx.room:room-ktx:2.6.1")
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.google.dagger:hilt-android:2.51")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
    kapt("com.google.dagger:hilt-compiler:2.51")
    kapt("androidx.room:room-compiler:2.6.1")
}

The plugins { } block declares which Gradle plugins apply to this module — the Android application plugin itself, the Kotlin Android plugin, the serialization compiler plugin, and Hilt's Gradle plugin. The android { } block configures Android-specific settings: which SDK version to compile against, the app's package name and version, and the defaultConfig values every build type inherits unless it overrides them. The dependencies { } block is the one you'll touch most often day to day — every library the module needs, one line each.

Build Types: Debug vs. Release, With Real Differences

The example above already shows the two build types every Android module ships with by default: debug and release. They're not just labels — each one can carry genuinely different configuration, and enabling R8 code shrinking and obfuscation only for the release build (as shown above with isMinifyEnabled = true inside the release { } block) is the single most common example. A debug build skips this deliberately: shrinking and obfuscating makes stack traces harder to read and slows down the fast iterate-and-rerun cycle a debug build is for, so it stays off there and only turns on for the build you actually ship. See ProGuard / R8 for what shrinking and obfuscation do in detail.

Product Flavors: Variants of the Same App

Product flavors solve a different problem than build types: shipping meaningfully different variants of the same app from one codebase, rather than different build configurations of the same variant. TrailLog might define a free flavor capped at a small number of saved trails alongside a pro flavor with no cap and cloud sync enabled:

android {
    flavorDimensions += "tier"
    productFlavors {
        create("free") { dimension = "tier" }
        create("pro") { dimension = "tier" }
    }
}

Gradle then multiplies flavors by build types automatically, so this configuration produces four actual build variants — freeDebug, freeRelease, proDebug, and proRelease — each independently buildable and installable side by side on one device.

Version Catalogs: One Place for Every Version Number

On a project with several modules (see Modularization), repeating a library's exact version string in every module's dependencies { } block gets error-prone fast — nothing stops :feature-trails and :core-database from silently drifting onto different Room versions. Gradle's version catalog solves this with a single gradle/libs.versions.toml file listing every version and library once:

[versions]
room = "2.6.1"
retrofit = "2.11.0"

[libraries]
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }

Every module then references the catalog instead of a hardcoded string — implementation(libs.room.ktx) — so bumping Room's version means editing exactly one line in the whole project, not hunting through every module's build script.

Note: Every build script shown here is correct, real Gradle Kotlin DSL as it would appear in an actual Android Studio project — but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.
Up next: With the build script covered, App Signing & Keystore covers the piece of configuration a release build type needs before Play Console will accept it.