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.
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.
On this page
When you maintain one app, a messy
build.gradle.ktsThe 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.
The
libs.versions.tomlgradle/[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:
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.
Compose ships many artifacts that must agree. Pinning each one by hand is how you get subtle crashes. The BOM aligns them for you:
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.
I keep the user-facing version in a tiny properties file, not buried in
build.gradle.kts# config/version.properties
VERSION_MAJOR=1
VERSION_MINOR=4
VERSION_PATCH=0
VERSION_CODE=42val 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"]}"
}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.
plugins {
alias(libs.plugins.ksp)
}
dependencies {
ksp(libs.hilt.compiler)
ksp(libs.room.compiler)
}Signing config reads from a gitignored file, never from the build script itself. The keystore and its passwords never touch version control.
val keystoreProps = Properties().apply {
val f = rootProject.file("keystore.properties")
if (f.exists()) load(f.inputStream())
}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
build-logicThe 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.
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.
libs.versions.tomlSudarshan 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
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read