Skip to content
All posts
July 9, 20264 min read

My Kotlin Gradle Build Setup With Version Catalogs

Keeping Gradle sane across 20+ Android apps means one source of truth for versions. Here's how I use version catalogs, convention plugins, and a shared version file to stop dependency drift before it starts.

GradleKotlinVersion CatalogAndroidBuild
Share:

When you maintain one app, a messy

code
build.gradle.kts
is annoying. When you maintain twenty, it's a tax you pay every single week. Different Compose versions here, a stale Kotlin there, a Hilt mismatch that only shows up at runtime. Version catalogs and a couple of conventions turned my build files from a liability into something I barely think about.

The goal behind all of it is simple to state and surprisingly hard to maintain by hand: every project should agree on the same versions, configured the same way, so that knowledge about how to build one app transfers directly to every other one. The moment two apps disagree about how they're built, you've created a special case, and special cases are what slowly make a large portfolio unmaintainable for a single person.

One Catalog, One Source of Truth

The

code
libs.versions.toml
file in
code
gradle/
is where every version and dependency lives. Nothing gets a hardcoded version string in a module's build file anymore.

toml
[versions]
kotlin = "2.3.21"
composeBom = "2025.12.01"
hilt = "2.56.2"
room = "2.8.4"

[libraries]
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }

[plugins]
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

In a module it reads cleanly, and the IDE autocompletes it:

kotlin
dependencies {
    implementation(libs.hilt.android)
    implementation(libs.room.runtime)
}

Bump a version in one place and every module that references it moves together. No more hunting for the one file you forgot.

Use the Compose BOM, Not Pinned Compose Versions

Compose ships many artifacts that must agree. Pinning each one by hand is how you get subtle crashes. The BOM aligns them for you:

kotlin
implementation(platform(libs.androidx.compose.bom))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")

I update one BOM coordinate and the whole Compose surface stays consistent.

App Version Lives Outside the Build File

I keep the user-facing version in a tiny properties file, not buried in

code
build.gradle.kts
, so a release script can bump it without parsing Kotlin.

properties
# config/version.properties
VERSION_MAJOR=1
VERSION_MINOR=4
VERSION_PATCH=0
VERSION_CODE=42
kotlin
val versionProps = Properties().apply {
    load(rootProject.file("config/version.properties").inputStream())
}
defaultConfig {
    versionCode = versionProps.getProperty("VERSION_CODE").toInt()
    versionName = "${versionProps["VERSION_MAJOR"]}." +
        "${versionProps["VERSION_MINOR"]}.${versionProps["VERSION_PATCH"]}"
}

KSP Over KAPT, Always

KAPT is deprecated and slow because it generates Java stubs. Every annotation processor I use — Hilt, Room — runs on KSP now. On a cold build the difference is minutes, not seconds.

kotlin
plugins {
    alias(libs.plugins.ksp)
}
dependencies {
    ksp(libs.hilt.compiler)
    ksp(libs.room.compiler)
}

Never Hardcode Signing Credentials

Signing config reads from a gitignored file, never from the build script itself. The keystore and its passwords never touch version control.

kotlin
val keystoreProps = Properties().apply {
    val f = rootProject.file("keystore.properties")
    if (f.exists()) load(f.inputStream())
}

Convention Plugins Stop You Repeating Yourself

Version catalogs solve the "which version" problem, but there's a second kind of duplication: every module configures the same Android block, the same Kotlin options, the same Compose setup. Copy that across twenty apps and a change to your baseline — say, bumping the JVM target — means twenty edits. Convention plugins fix this. I extract the shared configuration into a small plugin in

code
build-logic
and apply it everywhere, so the actual module build file shrinks to the few things that are genuinely unique to that module.

The effect is that a new module's build file becomes almost empty: apply the convention plugin, declare the dependencies that module actually needs, done. All the boilerplate that used to be copy-pasted now lives in exactly one place, and improving it improves every app at once. When a new Android Gradle Plugin changes a default, I update the convention plugin and every project inherits the fix the next time it builds.

The Payoff Across Many Apps

None of this matters much for a single hobby project. The reason I invest in it is scale. With more than twenty apps sharing the same conventions, I run a single script that bumps a dependency across all of them, or reports which apps are still on an old Compose BOM, precisely because every project reads its versions from the same catalog structure. Drift is the enemy of maintaining many apps, and a centralized build setup is the cheapest drift insurance there is.

There's also a real correctness benefit. Mismatched versions of Compose, Hilt, or Room don't always fail loudly at build time — sometimes they fail at runtime, on a user's device, in a way you can't reproduce. Pinning everything through one catalog and aligning the Compose surface through the BOM removes a whole category of "works on my machine" bugs before they can ship. The build setup is boring on purpose, and boring is what lets me spend my attention on the apps instead of the tooling that builds them.

Key Takeaways

  • Put every version and dependency in
    code
    libs.versions.toml
    so a bump happens in exactly one place.
  • Use the Compose BOM to keep Compose artifacts aligned instead of pinning each one.
  • Store the app version in a separate properties file so release scripts can bump it without editing Kotlin.
  • Use KSP for all annotation processing; KAPT is deprecated and noticeably slower.
  • Read signing credentials from a gitignored properties file — never hardcode them in the build.
Share:
S

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.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

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.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this