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
- Reading a build.gradle.kts (Kotlin DSL)
- The plugins and dependencies blocks
- implementation vs api vs testImplementation
- Version catalogs (libs.versions.toml)
- Tasks, modules and the Android Gradle Plugin
- Build types, flavors and the wrapper
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)
- "Could not resolve dependency" — Check the coordinate and version, and that the right repository is declared in settings.gradle.kts.
- Leaking dependencies with api — Use implementation unless consumers truly need the type.
- Mismatched Gradle versions — Always build through the ./gradlew wrapper, not a system Gradle.
- Mixing Groovy and Kotlin DSL syntax — In .kts files, strings need double quotes and calls need parentheses.
Pro Tips
- 💡 Centralize versions in libs.versions.toml so every module stays in sync.
- 💡 Split into modules as the app grows — Gradle builds independent modules in parallel.
- 💡 Commit the wrapper so teammates and CI build with the exact same Gradle version.
📋 Quick Reference — Gradle
| Concept | Kotlin DSL |
|---|---|
| Apply a plugin | plugins { kotlin("android") } |
| Internal dependency | implementation("group:lib:1.0") |
| Exposed dependency | api("group:lib:1.0") |
| Test dependency | testImplementation("junit:junit:...") |
| Version catalog | libs.versions.toml |
| Run a task | ./gradlew assembleDebug |
🎉 Lesson Complete!
- ✅ Gradle compiles, resolves dependencies, tests, and packages your app
- ✅ build.gradle.kts uses the type-safe Kotlin DSL
- ✅ implementation is internal; api is exposed; testImplementation is test-only
- ✅ Version catalogs centralize coordinates and versions
- ✅ The AGP, build types, flavors, and the wrapper power Android builds
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
- Previous: Dependency Injection with Hilt
- Next: Coroutine Exceptions & Cancellation — Handle failures and cancel cooperatively with SupervisorJob and handlers
- Quick reference: Kotlin cheat sheet
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.