Skip to content
All posts
October 11, 20266 min read

Android Gradle Version Catalogs in Depth

Discover how version catalogs eliminate version drift, streamline dependency updates, and keep 22+ Android projects consistent across a solo developer workflow.

AndroidKotlinToolsBest Practices
Share:

I spent weeks chasing a missing security patch only to realize my

code
build.gradle
files were out of sync. The pain of manual version updates across 22 apps became a daily bottleneck. Version catalogs are the solution that finally gave me a single source of truth for dependencies.

When you manage dozens of modules from a Bangkok‑based solo shop, inconsistent library versions are inevitable. You end up with

code
implementation "androidx.appcompat:appcompat:1.6.1"
in one module and
code
1.5.0
in another. This leads to runtime crashes, build failures, and a nightmare during Play Store releases. A version catalog centralizes these versions, enforces consistency, and reduces the mental load of tracking updates.

Why Version Catalogs Matter

ApproachMaintenance EffortVersion ConsistencyOnboarding New DevelopersRisk of Out‑of‑Date Libraries
Manual
code
build.gradle
entries
High – each module edited individuallyLow – easy to divergeSlow – each file must be inspectedHigh – human error
Version Catalog (
code
libs.versions.toml
)
Low – single file updatesHigh – one source of truthFast – catalog defines all versionsMinimal – automated resolution

A catalog turns dependency management from a scattered spreadsheet into a declarative TOML file. It also integrates with Gradle’s dependency resolution engine, so you get transitive version alignment for free.

Defining a Version Catalog

Create

code
gradle/libs.versions.toml
at the root of your project. This file declares library aliases and their versions in a format Gradle understands.

toml
[versions]
# Core AndroidX libraries
appcompat = "1.7.0"
material = "1.12.0"
constraintlayout = "2.2.0"
# Jetpack Compose
compose-bom = "2024.06.01"
compose-ui = "1.7.0"
compose-material = "1.2.0"
compose-tooling = "1.7.0"
# Testing
junit = "4.13.2"
androidx-junit = "1.2.1"
espresso-core = "3.6.1"
# DI
hilt = "2.51"
# Networking
retrofit = "2.11.0"
okhttp = "4.12.0"
# Utilities
kotlinx-coroutines = "1.8.0"
kotlinx-serialization = "1.7.0"

[libraries]
# AndroidX
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }

# Compose (BOM managed)
compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose-ui" }
compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose-material" }
compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose-tooling" }

# Testing
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.junit", name = "junit", version.ref = "androidx-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }

# DI
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }

# Networking
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlin-serialization", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }

# Utilities
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
android = { id = "com.android.application", version.ref = "android" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

[!TIP] Start with a minimal set – define only the libraries you currently use. Add new entries as your project grows; the catalog scales without extra overhead.

[!NOTE] Version references (

code
version.ref
) keep the file readable. You can update a single line and every module that uses the alias picks up the new version automatically.

[!WARNING] Avoid circular references – do not let a library version depend on another alias that itself references the first. Gradle will throw a resolution error.

[!IMPORTANT] Publish your catalog – if you maintain multiple apps, store the

code
libs.versions.toml
in a shared repository (e.g., a Git submodule) and apply it via
code
settings.gradle.kts
using
code
dependencyResolutionManagement
. This ensures every project inherits the same version matrix.

Using the Catalog in Your Modules

In each module's

code
build.gradle.kts
you reference the aliases directly. This removes repetitive version strings and makes upgrades a one‑file operation.

kotlin
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.dagger.hilt.android")
    id("org.jetbrains.kotlin.plugin.serialization")
}

android {
    namespace = "com.sudarshan.app"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.sudarshan.app"
        minSdk = 21
        targetSdk = 34
        versionCode = 1
        versionName = "1.0.0"
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = "17"
    }

    buildFeatures {
        compose = true
    }
}

dependencies {
    // Core AndroidX
    implementation(libs.androidx.appcompat)
    implementation(libs.androidx.material)
    implementation(libs.androidx.constraintlayout)

    // Compose (BOM is managed via libs.versions.toml)
    implementation(libs.compose.ui)
    implementation(libs.compose.material)
    implementation(libs.compose.tooling)

    // DI
    implementation(libs.hilt.android)
    kapt(libs.hilt.compiler)

    // Networking
    implementation(libs.retrofit)
    implementation(libs.retrofit.serialization)
    implementation(libs.okhttp)

    // Utilities
    implementation(libs.kotlinx.coroutines.android)
    implementation(libs.kotlinx.serialization.json)

    // Testing
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.espresso.core)
}

[!IMPORTANT] Keep

code
kapt
in sync – the catalog entry for
code
hilt-compiler
must match the
code
hilt
version. If you update the DI version, add a corresponding
code
hilt-compiler
entry.

[!TIP] Leverage

code
libs
object – Gradle automatically creates a
code
libs
object in Kotlin DSL projects. You can also use
code
libs.findLibrary("junit").get()
for optional dependencies.

[!NOTE] BOM handling – the Compose BOM is declared as a separate version (

code
compose-bom
). You can optionally import it with
code
implementation(platform(libs.compose.bom))
to centralize version constraints for all Compose libraries. This pattern reduces the risk of version mismatches across UI components.

Best Practices and Gotchas

PracticeWhy It MattersHow to Implement
Single source of truthPrevents divergent versions across modulesStore
code
libs.versions.toml
in a shared location (e.g., a monorepo root)
Alias naming consistencyMakes search and refactoring easyUse
code
androidx‑*
for AndroidX,
code
compose‑*
for Compose,
code
hilt‑*
for DI
Separate version and library blocksImproves readabilityKeep
code
[versions]
for numbers,
code
[libraries]
for coordinates
Version alignment with pluginsAvoids plugin‑library mismatchesAdd plugin versions under
code
[plugins]
and reference them in
code
settings.gradle.kts
Regular catalog auditsKeeps dependencies secure and up‑to‑dateRun
code
./gradlew dependencyUpdates
weekly and update the catalog accordingly

[!WARNING] Do not mix

code
implementation
and
code
api
indiscriminately
– using
code
api
exposes transitive dependencies to consumers of your library, which can cause unexpected version conflicts in multi‑module projects.

[!TIP] Use

code
dependencyResolutionManagement
in
code
settings.gradle.kts
to enforce the catalog globally:

kotlin
dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

[!IMPORTANT] Version catalogs are not a substitute for testing – always run the full test suite after updating a library version. A catalog only guarantees version alignment; it does not guarantee runtime compatibility.

Key Takeaways

  • Centralize all library versions in a single
    code
    libs.versions.toml
    file to eliminate drift across 22+ Android projects.
  • Reference aliases directly in
    code
    build.gradle.kts
    to simplify dependency updates and improve onboarding speed.
  • Apply best‑practice patterns such as consistent naming, plugin version alignment, and regular audits to keep the catalog maintainable.
  • Use Gradle’s
    code
    dependencyResolutionManagement
    to share a catalog across multiple modules or even separate repositories.
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