Why Build Speed Matters More Than You Think
Diagnosing With Build Scans and Profile Reports
# Generate a Gradle profile report (local)
./gradlew assembleDebug --profile
# Generate a build scan (hosted on scans.gradle.com)
./gradlew assembleDebug --scan
# Common findings in build scans:
# 1. Configuration phase > 5s → too many plugins
# or dynamic dependency resolution
# 2. Cache miss rate > 30% → unstable task inputs
# 3. A single task taking > 30% of build time
# → investigate that task specifically
# 4. No parallel execution → missing configuration
# Quick diagnostic: compare clean vs incremental
time ./gradlew clean assembleDebug # Clean build
time ./gradlew assembleDebug # Incremental
# If incremental is not significantly faster,
# caching/incrementality is brokenConfiguration Cache and Parallel Execution
# gradle.properties - Core performance settings
# Enable configuration cache (Gradle 8.1+)
org.gradle.configuration-cache=true
# Show problems as warnings during adoption
org.gradle.configuration-cache.problems=warn
# Parallel execution for multi-module projects
org.gradle.parallel=true
# Gradle daemon keeps JVM warm between builds
org.gradle.daemon=true
# Increase daemon heap for large projects
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError \
-Dfile.encoding=UTF-8 \
-XX:MaxMetaspaceSize=512m
# Build cache (local by default)
org.gradle.caching=true
# Non-transitive R classes (AGP 8.0+)
# Prevents R class changes from cascading rebuilds
android.nonTransitiveRClass=true
# Disable unused Android features
android.defaults.buildfeatures.aidl=false
android.defaults.buildfeatures.buildconfig=false
android.defaults.buildfeatures.renderscript=false
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=falseDependency Management for Faster Resolution
# gradle/libs.versions.toml - Centralized version catalog
[versions]
kotlin = "2.1.0"
compose-bom = "2026.01.00"
hilt = "2.51.1"
room = "2.7.0"
ktor = "3.0.3"
coroutines = "1.9.0"
[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
[bundles]
compose = ["compose-ui", "compose-material3"]
room = ["room-runtime", "room-ktx"]
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }Module Structure and Build Avoidance
// core/model/build.gradle.kts
// This module's public API is ONLY data classes
// used across features. Keep it tiny.
plugins {
id("com.android.library")
alias(libs.plugins.kotlin.android)
}
dependencies {
// api: exposed to consumers because Task
// appears in public function signatures
api(libs.kotlinx.datetime)
// implementation: internal to this module,
// changes here don't trigger downstream rebuilds
implementation(libs.kotlinx.serialization.json)
}
// feature/tasks/build.gradle.kts
dependencies {
// Only depend on the interface module, not
// the implementation -- faster builds
implementation(project(":core:model"))
implementation(project(":core:data-api"))
// NOT this: pulls in all of :core:data
// and its transitive dependencies
// implementation(project(":core:data"))
}CI-Specific Optimizations
# .github/workflows/android-ci.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Build debug APK
run: >
./gradlew assembleDebug
--no-daemon
--build-cache
--parallel
-Porg.gradle.workers.max=4
-Porg.gradle.jvmargs="-Xmx4g"
- name: Run unit tests
run: >
./gradlew testDebugUnitTest
--no-daemon
--build-cache
--parallel
- name: Upload build reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: build-reports
path: "**/build/reports/"Key Takeaways
- 1Run a build scan before optimizing -- never guess where build time is spent.
- 2Configuration cache + parallel execution often deliver 30-50% improvement with zero code changes.
- 3Pin dependency versions in a version catalog to avoid dynamic resolution overhead.
- 4Use implementation instead of api for dependencies to prevent cascading rebuilds.
- 5Keep shared module API surfaces small -- only expose types that consumers actually need.
- 6Configure CI separately: disable daemon, enable remote cache, maximize worker threads.
Frequently Asked
Why is my Gradle configuration so slow?
Common causes: too many plugins, complex build logic in build.gradle, missing configuration cache. Move logic to convention plugins or precompiled script plugins.
How do I speed up dependency resolution?
Use dependency locking. Configure repositories efficiently (remove unused repos). Use Gradle's dependency verification. Consider a local artifact cache like Gradle Enterprise.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.