MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
After 13 years of Android builds and 22 shipped apps, here are the Gradle optimizations that genuinely cut my build times — not theoretical advice, just the knobs I actually turned and the results I measured.
On this page
I shipped my first Android app in 2012, when Gradle was still the new kid on the block and clean builds took a coffee break. Thirteen years and 22 apps later, I have spent more hours than I care to admit staring at a spinner. Most "speed up your Gradle build" posts recycle the same five tips. This one is different. I am only writing about the changes that actually moved the needle on real projects, with the numbers I measured.
If you maintain a medium to large Android codebase and your CI feels like a tax, this is the playbook.
Before tuning anything, I profiled. Gradle's
--profile:app:processDebugResourcesOn my current project, a single-module app with around 90k lines of Kotlin, the baseline numbers looked like this:
| Build type | Cold (no cache) | Warm (incremental) |
|---|---|---|
| Debug assemble | 4m 12s | 38s |
| Release assemble | 5m 48s | 51s |
| Unit tests | 2m 30s | 22s |
| Lint | 3m 10s | 1m 05s |
Two things jumped out. First, KAPT was eating a huge slice of configuration time. Second, every clean build downloaded and processed the same dependencies from scratch, even when nothing in
build.gradle.kts[!NOTE] Always run with
before optimizing. Without data, you are guessing. Guessing with Gradle is a great way to spend a day making things worse.code--profile
I tried roughly a dozen tweaks over six months. Five made a measurable difference. The rest were either neutral, already enabled, or actively harmful. Here is what worked, in order of impact.
KAPT runs the Java annotation processor pipeline for every Kotlin file. It is slow, it duplicates work, and it has been officially discouraged for years. KSP does the same job by reading Kotlin's own AST directly.
The migration for my Hilt and Room setup took an afternoon. The configuration change:
// build.gradle.kts (project)
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
// build.gradle.kts (app)
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation("androidx.room:room-runtime:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
implementation("com.google.dagger:hilt-android:2.51.1")
ksp("com.google.dagger:hilt-android-compiler:2.51.1")
// Replace any kapt(...) lines with ksp(...)
}[!IMPORTANT] If you still use kapt anywhere, the build pays a tax on every single Kotlin compilation. The KSP migration was the single biggest win I have ever seen on Android build performance.
Cold build dropped from 4m 12s to 3m 04s on the same machine. Configuration time alone fell by roughly 40 percent.
Gradle's build cache stores task outputs by their inputs. Reuse them and you skip work that has already been done. The trick is making sure the cache actually has a home and CI knows about it.
Locally, I set a fixed cache directory so different projects stop fighting over the default location. On CI, I point at a remote cache backed by object storage.
// settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
// Enable build cache and configure remote cache for CI
buildCache {
local {
directory = File(System.getProperty("user.home"), ".gradle/caches/build-cache")
removeUnusedEntriesAfterDays = 30
}
remote(HttpBuildCache::class.java) {
url = uri("https://cache.example.com/gradle-cache/")
isAllowInsecureProtocol = false
// Push only from CI to avoid local machines spamming the remote
isPush = System.getenv("CI") != null
credentials {
username = System.getenv("CACHE_USER") ?: ""
password = System.getenv("CACHE_TOKEN") ?: ""
}
}
}On CI, warm build times fell from around 90 seconds to under 25 seconds because the same Maven artifacts and intermediate outputs from the previous run were reused.
[!TIP] Use a separate
directory per project if you work on several. The default cache mixes projects, and a corrupted entry from one repo will poison another.code~/.gradle/caches/build-cache
For years my projects were single-module. It is the path of least resistance, and the path of slowest builds. Every change recompiled the entire codebase.
I split the project into a
:core:app// settings.gradle.kts
include(":app")
include(":core")
include(":core:network")
include(":core:database")
include(":feature:home")
include(":feature:settings")
include(":feature:billing")
// feature:home/build.gradle.kts
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.sudarshantechlabs.feature.home"
compileSdk = 35
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(project(":core"))
implementation(project(":core:database"))
implementation("androidx.compose.ui:ui:1.7.5")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}The first time I touched only
feature:homefeature:billingcore:network[!WARNING] Module splits have a real cost. Every
symbol you want to share has to either becomecodeinternalor move to acodepublicmodule. Decide before you start splitting. A messy split is worse than a slow single module.code:core
Some features look free and are not. In every project I run, I now disable three things by default: BuildConfig generation, vector drawable rasterization for old API levels, and the resource shrinker on debug builds.
// app/build.gradle.kts
android {
buildFeatures {
buildConfig = false
// Only enable when you actually use it
// aidservice = true
}
defaultConfig {
// minSdk is 24 in my projects, so PNG rasterization is dead weight
vectorDrawables {
useSupportLibrary = false
}
}
buildTypes {
debug {
isMinifyEnabled = false
isShrinkResources = false
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}buildConfig = falseTwo Gradle properties do most of the heavy lifting. Add them to
gradle.properties# gradle.properties
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+UseParallelGC
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=trueorg.gradle.parallel=trueorg.gradle.configureondemand=true[!TIP] If you use Android Studio, also bump
to at least 4 GB. Default heap allocation is 512 MB, which forces frequent GC pauses and is a leading cause of "the IDE is sluggish" complaints that are actually Gradle complaints.code-Xmx
I want to be honest about the things that did not matter.
Switching from Groovy to Kotlin DSL in
build.gradleRemoving
apply pluginplugins {}Adding
--offlineGenerating sources once via
ksp { arg("room.schemaLocation", ...)}After all changes, the same project looks like this:
| Build type | Before | After | Improvement |
|---|---|---|---|
| Debug assemble (cold) | 4m 12s | 2m 18s | 45 percent faster |
| Debug assemble (warm) | 38s | 9s | 76 percent faster |
| Release assemble (cold) | 5m 48s | 3m 30s | 40 percent faster |
| Unit tests (cold) | 2m 30s | 1m 18s | 48 percent faster |
| Lint | 3m 10s | 1m 42s | 46 percent faster |
The warm build number is what changed my day-to-day life. Sub-10-second builds on a medium-sized app means I stay in flow. I do not context-switch. I do not check Slack while waiting. That is worth more than any of the headline cold-build savings.
~/.gradle/caches/build-cachebuildConfiggradle.propertiesorg.gradle.parallel=trueorg.gradle.caching=true-Xmx4gThe boring truth is that build performance is a stack. The base layer is what Gradle is asked to do, and that is shaped by your build files. The middle layer is parallelism and caching. The top layer is your machine. Fix the base first. Everything else is decoration.
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus