Build tooling history
Eclipse and Ant, then Android Studio and Gradle, then Kotlin DSL and version catalogs. Every switch replaced the thing that had become the bottleneck, and each one kept the old output format alive.
For most of Android's life, "building the app" meant one button in an IDE that did an unknowable number of things. The history of build tooling is the history of making that button explain itself.
TL;DR#
- From 2008 to 2013 the toolchain was Eclipse + ADT + Ant: XML targets, no dependency graph, and no concept of a build variant.
- Android Studio (announced 2013, 1.0 in December 2014) moved the IDE onto Gradle, and the Android Gradle Plugin taught Gradle what an APK is.
- Gradle's real gift was not speed. It was a task graph, which made incremental builds, variants and dependency resolution possible at all.
- The toolchain underneath changed separately: dx → D8 for dexing, and ProGuard → R8 for shrinking, optimisation and obfuscation.
- Build files themselves changed from Groovy to Kotlin DSL, and version numbers moved into a single version catalog. Neither changes what an APK contains.
Mental model#
Think of a shipyard that has been rebuilt three times around the same ship design.
The slipway is the build system. The first one was a set of hand-pulled rails: it worked, but nothing told you what would happen until you pulled. The second was a rail network with switches — Gradle's task graph — where you could ask "what trains are scheduled?" before any of them left.
The yard tools are the compiler, the dexer and the shrinker. They got replaced more often than the slipway, because the ship's materials changed faster than its shape.
The shipping paperwork — the APK format, the manifest, the signing scheme — barely changed at all. That is the point: you can rebuild the yard completely and still launch the same ship.
Where the analogy breaks#
A shipyard is rebuilt once and then runs for decades. Android's build tooling has never stopped
changing: a project created in 2013 and a project created in 2024 use the same Gradle, but almost
nothing else in common. The migration is continuous, and most real codebases are a mixture of three
eras at once — an old module still on Groovy, a new module on Kotlin DSL, and a buildSrc folder
that belongs to neither.
The big picture#
Text description of this diagram
A timeline from 2009 to 2024 across three lanes. The IDE lane runs Eclipse ADT (2009), Android Studio announced in 2013 and reaching 1.0 in December 2014, Android Studio 3.0 in 2017 with Kotlin support. The build system lane runs Ant with the ADT plugin in 2009, Gradle and the Android Gradle Plugin from 2013, Instant Run in 2015, Kotlin DSL around 2019, version catalogs in 2021, and configuration cache maturing in 2021. The toolchain lane runs the dx dexer in 2009, D8 in 2017, R8 replacing ProGuard in 2018, KSP in 2021 and the K2 compiler in 2024. Against a backdrop of three eras: the Eclipse ADT era, the Gradle era, and the Kotlin DSL and modern AGP era.
The timeline's most striking feature is that the compiler lane changes at a different rhythm from the IDE lane. The IDE has had two real phases — Eclipse and Studio. The dexer has had three. The build system has had essentially one since 2013, even though how you write its files has changed twice.
Deep dive#
The Ant era: a build script is a list of steps#
An Ant build is a sequence of named targets with dependencies between them. It is a scripting language for file operations: copy these resources, invoke this tool, zip the result. There is no model of "an Android app" anywhere in it.
That has two consequences. First, nothing can be incremental for you — if you want to skip a
step, you write the timestamp check yourself. Second, nothing can be reused — every project
wrote its own build script, and every one was slightly different. Adding a library meant downloading
a JAR and putting it in a libs/ folder.1
The ADT plugin made this bearable by generating the script for you. That is also why the build was opaque: the thing you edited and the thing that ran were both generated, and neither was documented in the way a programming language is.
2013–2014: Gradle arrives with a model#
Gradle brought three things Ant could not express.
A task graph. Tasks declare inputs and outputs; Gradle decides what must run. That single change made incremental builds possible as a general property rather than a per-project hack.
Dependency resolution with conflict resolution. implementation("androidx.core:core-ktx:1.13.1")
is a graph edge, not a file copy. If two modules ask for different versions, Gradle picks one and
tells you it did.
Build types and product flavours. This is the concept Android calls a : the cross product of how you build (debug, release) and what you build (free, paid). A project with two build types and two flavours has four variants, four merged manifests, and four APKs. Ant had no way to express this at all.2
The Android Gradle Plugin is what makes this Android-specific. Gradle alone knows about Java
tasks; AGP adds assembleRelease, bundleRelease, resource processing, manifest merging, dexing
and signing. The plugin and the build tool are separate projects with a compatibility matrix
between them — a fact that every upgrade eventually teaches you.
The toolchain beneath: dx to D8, ProGuard to R8#
The build system describes what to run; the tools do the work. Those changed on their own schedule.
dx → D8. dx converted JVM class files to DEX. D8 replaced it and, crucially, took over desugaring: rewriting Java 8 language features — lambdas, default methods, try-with-resources — into forms older Android versions can execute. That is what made it possible to use modern Java syntax while still supporting old devices.3
ProGuard → R8. ProGuard shrank and obfuscated, then a separate dexer ran afterwards. R8 does shrinking, optimisation, obfuscation and dexing in a single pass. Fewer passes over the bytecode means faster builds and, often, smaller output.4
Both replacements shared a design constraint: the old configuration format had to keep working.
R8 still reads ProGuard rules, because the entire ecosystem had ten years of them. That is why your
proguard-rules.pro file is still called that.
Groovy to Kotlin DSL#
Gradle build files were Groovy. Groovy is dynamic, terse and forgiving — which is exactly why build files written in it are hard to refactor and hard for an IDE to complete.
The Kotlin DSL (build.gradle.kts) makes the build script a statically typed Kotlin program. The
practical difference is not elegance; it is that your IDE can tell you that minSdk is an Int
before you run anything.
// build.gradle.kts
android {
namespace = "com.example.app"
compileSdk = 36
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}Nothing above is a new capability. Every line has a Groovy equivalent. What changed is that a typo
in isMinifyEnabled is now a compile error in the build script rather than a silently ignored
property.
Version catalogs#
The last era's problem was not capability but coordination. A large project had the same version string in eight build files, and the AndroidX BOM was a partial answer to only one dependency family.
A puts every version in one TOML file:
# gradle/libs.versions.toml
[versions]
kotlin = "2.0.21"
coroutines = "1.9.0"
[libraries]
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }dependencies {
implementation(libs.kotlinx.coroutines.android)
testImplementation(libs.kotlinx.coroutines.test)
}The catalog is generated into a type-safe accessor, so libs.kotlinx.coroutines.android completes in
the IDE and fails at configuration time if the alias does not exist. It also makes version drift
visible: one number, one place.
What "build speed" actually means#
The phrase hides three different problems, and each era solved a different one.
Incremental correctness is whether a rebuild after a one-line change produces the same artifact as a clean build. Ant had no answer; Gradle's input and output declarations are the answer, and an undeclared input is the bug.
Configuration cost is the fixed price of every invocation, independent of what changed. It grew as projects became modular and eventually became the dominant cost for large builds. The configuration cache is the answer.
Tool throughput is how fast the compilers and dexers run on the work they are given. Kotlin's K2 front end and D8's replacement of dx are answers here.
When someone says "Gradle is slow", the useful follow-up is: slow to start, slow to notice a change, or slow to do the work? The three have different fixes, and only one of them is a faster laptop.
How it evolved#
The constant across all three eras: the output format never changed. An APK built in 2013 and one built today are both ZIP files with a manifest, resources and DEX inside. The signing scheme evolved — v2 in Android 7, v3 after that — but the container did not. That stability is why the tooling could be replaced without breaking the Play Store.
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 4 | 1.6 Donut |
|
| 5 | 2.0 Eclair |
|
| 8 | 2.2 Froyo |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 13 | 3.2 Honeycomb |
|
| 14 | 4.0 Ice Cream Sandwich |
|
| 16 | 4.1 Jelly Bean |
|
| 17 | 4.2 Jelly Bean |
|
| 18 | 4.3 Jelly Bean |
|
| 19 | 4.4 KitKat |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 22 | 5.1 Lollipop |
|
| 23 | 6.0 Marshmallow |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 27 | 8.1 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 32 | 12L |
|
| 33 | 13 Tiramisu |
|
| 34 | 14 Upside Down Cake |
|
| 35 | 15 Vanilla Ice Cream |
|
| 36 | 16 Baklava |
|
| 37 | 17 Cinnamon Bun |
|
Under the hood#
The build history is legible in four files that every Android project still has:
- build.gradle.ktsplugin versions and repositories; the root project's script
- build.gradle.ktscompileSdk, build types and dependencies for one module
- libs.versions.tomlthe version catalog, if the project has adopted one
- gradle-wrapper.propertiesthe Gradle version, pinned for everyone who builds
Those are paths in your own project, not in AOSP, and that is the point: the application build
system is a different world from the platform build system, which is its own story.
The one file that has survived every era is the manifest — AndroidManifest.xml has been read by
every build system Android has ever had, because it is what the platform installs against, not what
the build tool invents.
Curiosity corner#
- The Gradle wrapper is the only build tool most teams version-control.
gradlewandgradle-wrapper.propertiesare committed so that every machine uses the same Gradle, without anyone installing Gradle. - "Instant Run" was the most ambitious failure in Android tooling. It tried to hot-swap code into a running app; the constraints of ART made it unreliable, and it was replaced by Apply Changes, which is honest about what it can and cannot do.
proguard-rules.prosurvives R8 because the rule format, not the tool, was the contract.
Try it#
./gradlew :app:tasks --all
Example output
./gradlew :app:tasks --all
> Task :app:tasks
------------------------------------------------------------
All tasks runnable from project ':app'
Android tasks
-------------
assembleDebug - Assembles main output for variant debug
assembleRelease - Assembles main output for variant release
bundleRelease - Assembles bundle for variant release
..../gradlew :app:assembleRelease --dry-run
Example output
:app:preBuild SKIPPED
:app:preReleaseBuild SKIPPED
:app:mergeReleaseResources SKIPPED
:app:processReleaseResources SKIPPED
:app:compileReleaseKotlin SKIPPED
:app:minifyReleaseWithR8 SKIPPED
:app:packageRelease SKIPPED
:app:assembleRelease SKIPPEDThe SKIPPED lines are not failures. --dry-run prints the graph and executes nothing, which is
why it finishes in seconds instead of minutes.
Build it#
Run ./gradlew :app:assembleRelease --dry-run on any Android project you have and count the tasks
before assembleRelease. Then open the build pipeline and match each task
to a stage.
Check yourself#
Check yourself
Put these build stages in order, from first to last.
Drag, or use the arrows to reorder.
- 1AAPT2 link produces resources.arsc and the R class
- 2D8 converts class files to DEX
- 3kotlinc compiles Kotlin to JVM class files
- 4apksigner writes the v2/v3 signature blocks
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- wikiAndroid version historyWikipediaaccessed 2026-10-27↩
- docsConfigure your builddeveloper.android.comaccessed 2026-10-27↩
- docsJava 8+ API desugaring supportdeveloper.android.comaccessed 2026-10-27↩
- docsShrink, obfuscate, and optimize your appdeveloper.android.comaccessed 2026-10-27↩
- docsOptimize your build speeddeveloper.android.comaccessed 2026-10-27↩
- docsGradle user manualdocs.gradle.orgaccessed 2026-10-27↩
- docsapksignerdeveloper.android.comaccessed 2026-10-27↩
- docsBuild Androidsource.android.comaccessed 2026-10-27↩