Gradle & the Kotlin Build System

Reviewed & published by Brayan K

Gradle is the build tool behind almost every Kotlin and Android project. It compiles your code, resolves dependencies, runs tests, and packages your app — all driven by readable build scripts.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end you'll read a build.gradle.kts, understand dependency configurations, and use a libs.versions.toml catalog.

What You'll Learn in This Lesson

1️⃣ Anatomy of build.gradle.kts

A module's build script applies plugins, configures the build (here the android block from the Android Gradle Plugin), and declares dependencies. The Kotlin DSL gives you type safety and autocompletion.

// build.gradle.kts — a module's build script in the Kotlin DSL.

plugins {
    // Apply plugins declaratively.
    id("com.android.application")
    kotlin("android")
}

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

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

dependencies {
    implementation("androidx.core:core-ktx:1.13.1")
    testImplementation("junit:junit:4.13.2")
}

Running ./gradlew assembleDebug executes the task graph and produces your APK.

2️⃣ Dependency Configurations

The configuration you choose controls where a library is visible. implementation keeps it internal; api exposes it to consumers; testImplementation is test-only.

dependencies {
    // implementation: used to compile THIS module; hidden from consumers.
    implementation("com.squareup.okhttp3:okhttp:4.12.0")

    // api: also EXPOSED to modules that depend on this one.
    api("com.google.code.gson:gson:2.11.0")

    // testImplementation: only on the test classpath.
    testImplementation("junit:junit:4.13.2")

    // runtimeOnly: present at runtime but not at compile time.
    runtimeOnly("org.slf4j:slf4j-simple:2.0.13")
}

Reaching for implementation by default keeps your module's surface small and your builds fast.

3️⃣ Version Catalogs

A version catalog in gradle/libs.versions.toml centralizes every coordinate and version. Your scripts then reference them type-safely as libs.*, so upgrades happen in one place.

# gradle/libs.versions.toml — a version catalog.
[versions]
kotlin = "2.0.0"
coreKtx = "1.13.1"

[libraries]
core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }

[plugins]
android-app = { id = "com.android.application", version = "8.5.0" }

Your turn. Replace the TODOs, then compare.

// 🎯 YOUR TURN — replace each TODO in build.gradle.kts.

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

dependencies {
    // 1) Add a normal compile dependency (internal to this module).
    TODO("androidx.core:core-ktx:1.13.1")    // 👉 implementation(...)

    // 2) Add JUnit, but ONLY for tests.
    TODO("junit:junit:4.13.2")               // 👉 testImplementation(...)
}

Mini-Challenge: Build Types & Flavors

Configure a release build type and a free/paid flavor dimension to produce multiple variants.

// 🎯 MINI-CHALLENGE: a multi-flavor build.gradle.kts.
//
// 1. In the android { } block, add two build types implicitly
//    (debug is default) and enable minification for release:
//      buildTypes { release { isMinifyEnabled = true } }
// 2. Add a product flavor dimension "tier" with two flavors:
//      free  -> applicationIdSuffix = ".free"
//      paid  -> applicationIdSuffix = ".paid"
// 3. Add one 'implementation' and one 'testImplementation' dependency.
//
// ✅ Gradle now produces variants like freeDebug, paidRelease, etc.

android {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Gradle

ConceptKotlin DSL
Apply a pluginplugins { kotlin("android") }
Internal dependencyimplementation("group:lib:1.0")
Exposed dependencyapi("group:lib:1.0")
Test dependencytestImplementation("junit:junit:...")
Version cataloglibs.versions.toml
Run a task./gradlew assembleDebug

🎉 Lesson Complete!

Practice quiz

What is the file extension for a Gradle build script written in the Kotlin DSL?

  • .gradle.kts
  • .kts.build
  • .kotlin
  • .gradle

Answer: .gradle.kts. build.gradle.kts uses the Kotlin DSL, giving type safety and IDE autocompletion over the older Groovy build.gradle.

Which block do you use to apply Gradle plugins?

  • modules { }
  • use { }
  • plugins { }
  • apply { }

Answer: plugins { }. The plugins { } block is the modern, declarative way to apply plugins such as the Android Gradle Plugin or the Kotlin plugin.

Which dependency configuration exposes a dependency to modules that depend on yours?

  • testImplementation
  • api
  • runtimeOnly
  • implementation

Answer: api. api leaks the dependency onto the consumer's compile classpath; implementation keeps it internal for faster builds.

What does the 'implementation' configuration do compared with 'api'?

  • It only adds it at runtime
  • It is for test code only
  • It makes the dependency public to consumers
  • It keeps the dependency internal to the module, so it is not exposed to consumers

Answer: It keeps the dependency internal to the module, so it is not exposed to consumers. implementation hides the dependency from downstream modules, which improves build performance and encapsulation.

Which configuration adds a dependency only to the test source set?

  • testImplementation
  • implementation
  • api
  • compileOnly

Answer: testImplementation. testImplementation adds libraries like JUnit that are needed to compile and run tests but not the production code.

What is a Gradle version catalog typically defined in?

  • catalog.xml
  • libs.versions.toml
  • settings.json
  • versions.gradle

Answer: libs.versions.toml. A version catalog in gradle/libs.versions.toml centralizes dependency coordinates and versions, referenced as libs.* in scripts.

What does the Gradle wrapper (gradlew) provide?

  • A faster JVM
  • Automatic publishing
  • A GUI for Gradle
  • A pinned Gradle version so everyone builds with the same toolchain

Answer: A pinned Gradle version so everyone builds with the same toolchain. The wrapper downloads and runs a specific, project-pinned Gradle version, so builds are reproducible across machines and CI.

What is the Android Gradle Plugin (AGP) responsible for?

  • Running the emulator
  • Writing your Kotlin code
  • Adding Android build capabilities like assembling APKs/AABs and merging resources
  • Hosting the app store

Answer: Adding Android build capabilities like assembling APKs/AABs and merging resources. The AGP teaches Gradle how to build Android apps: compiling resources, packaging APK/AAB outputs, and configuring variants.

What are Android build types like 'debug' and 'release' used for?

  • Choosing a programming language
  • Configuring how the app is built and packaged, e.g. minification and signing
  • Selecting the emulator
  • Setting the app name only

Answer: Configuring how the app is built and packaged, e.g. minification and signing. Build types let you vary settings such as code shrinking, signing, and debuggability between development and production builds.

What is a unit of work that Gradle executes, like assembleDebug, called?

  • A task
  • A flavor
  • A module
  • A plugin

Answer: A task. Gradle models a build as a directed graph of tasks; each task (e.g. compileKotlin, assembleDebug) does a discrete piece of work.

Continue this course

Frequently asked questions

What is Gradle and why does Kotlin/Android use it?

Gradle is the build automation tool that compiles your code, resolves and downloads dependencies, runs tests, and packages your app. It models a build as a graph of tasks (like compileKotlin or assembleDebug) and only re-runs what changed, which keeps builds fast. Android and most Kotlin projects use Gradle because it's flexible, has a huge plugin ecosystem, and — through the Android Gradle Plugin — knows how to turn your sources and resources into installable APK or AAB outputs.

What's the difference between the Groovy and Kotlin DSL?

Historically Gradle scripts were written in Groovy (build.gradle). The Kotlin DSL (build.gradle.kts) lets you write the same scripts in Kotlin, which gives you static typing, real IDE autocompletion, and compile-time error checking in your build logic. For a Kotlin project it's the natural choice: the same language you write your app in configures your build. The blocks you'll see most are plugins { } to apply plugins, android { } to configure the Android build, and dependencies { } to declare libraries.

What do implementation, api, and testImplementation mean?

These are dependency configurations that control where a library is visible. implementation adds a dependency for compiling the current module but hides it from modules that depend on yours — this is the sensible default because it improves encapsulation and lets Gradle rebuild less. api also exposes the dependency to downstream consumers, which you only want when your public API actually returns or accepts types from that library. testImplementation puts a dependency (like JUnit) only on the test classpath, so it never ships in your production build. There are others too, such as runtimeOnly and compileOnly, for finer control.

What are version catalogs and the Gradle wrapper?

A version catalog, defined in gradle/libs.versions.toml, is a single place to declare all your dependency coordinates and versions. Your build scripts then reference them type-safely as libs.core.ktx or libs.plugins.android.app, so upgrading a library means editing one line and every module stays consistent. The Gradle wrapper (the gradlew script plus its properties) pins a specific Gradle version to the project and downloads it on demand, so every developer and your CI build with the exact same toolchain — no 'works on my machine' surprises. Together with multi-module setups, build types, and product flavors, these are the building blocks of a real Android/Kotlin build.