AppMama

blog / gradle

Gradle Version Catalogs in Android: A Complete Guide

· 9 min read · AppMama

A Gradle version catalog is a single TOML file, gradle/libs.versions.toml, that lists every library, plugin and version your Android project uses. Build scripts then refer to entries through generated, type-safe accessors such as libs.androidx.core.ktx instead of hard-coded strings. This guide covers the file format, bundles, plugins, BOMs, convention plugins and how to migrate from ext blocks or buildSrc.

If you create a new project in a recent version of Android Studio, you already have a catalog. If you are maintaining an older project, the migration is incremental and low-risk: a build can mix catalog accessors and plain strings, so you can move one dependency at a time.

What is a Gradle version catalog?

Gradle reads gradle/libs.versions.toml by convention and generates an extension called libs that every project in the build can use. The catalog does three jobs:

A catalog is a list of declarations, not a constraint system. Declaring room = "2.8.5" requests that version; normal Gradle conflict resolution still applies, so a transitive dependency asking for something newer can win. If you need to force a version, that is a job for dependency constraints or a platform, not the catalog.

What goes in libs.versions.toml?

The file has four sections. All are optional, but most Android projects use all four.

gradle/libs.versions.toml

[versions]
agp = "9.5.0"
kotlin = "2.4.21"
coreKtx = "1.19.1"
lifecycle = "2.11.0"
activityCompose = "1.13.0"
composeBom = "2026.09.00"
room = "2.8.5"

[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }

# Compose: versions come from the BOM
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }

androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

[bundles]
compose = ["androidx-compose-ui", "androidx-compose-ui-tooling-preview", "androidx-compose-material3"]
room = ["androidx-room-runtime", "androidx-room-ktx"]

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

The versions above were current stable releases in November 2026. Check the AndroidX releases page and the AGP release notes for the latest before copying them.

[versions]

Named version strings that other entries point at with version.ref. Give a version its own name only when two or more entries share it, or when you need it in a build script (for example compileSdk). For a one-off library, an inline version = "1.2.3" is fine and keeps the file shorter.

A version can also be a rich version table, using the same keywords as Gradle’s rich versions: require, strictly, prefer, reject and rejectAll. You rarely need these in an app, but strictly is occasionally useful to pin a library that keeps getting upgraded transitively.

[libraries]

Each entry is an alias mapped to coordinates. Three forms are accepted:

a = "com.example:lib:1.0"                                      # one string
b = { module = "com.example:lib", version.ref = "lib" }        # module + version
c = { group = "com.example", name = "lib", version.ref = "lib" } # group + name

Use whichever you like, but pick one style for the whole file. Entries with no version at all are valid and are how you declare BOM-managed artifacts.

[bundles]

A bundle is a named list of library aliases that you always add together. implementation(libs.bundles.room) adds every library in the list. Bundles only contain libraries: you cannot put plugins in them, and you cannot wrap a BOM in platform() inside one, so the BOM line stays separate.

Keep bundles small and honest. A bundle called compose that sneaks in debug tooling or test libraries becomes a trap, because you can only add a whole bundle to one configuration.

[plugins]

Plugin entries take an id and a version. You apply them with alias() in a plugins {} block, never with id().

How do catalog names map to accessors?

Gradle splits aliases on dashes, underscores and dots, and turns each segment into a level of nesting. So androidx-lifecycle-runtime-ktx becomes libs.androidx.lifecycle.runtime.ktx. Plugins live under libs.plugins, bundles under libs.bundles, and versions under libs.versions.

TOML aliasKotlin DSL accessor
androidx-core-ktxlibs.androidx.core.ktx
androidx_room_runtimelibs.androidx.room.runtime
coreKtx (in [versions])libs.versions.coreKtx
compose (in [bundles])libs.bundles.compose
android-application (in [plugins])libs.plugins.android.application

Android’s documentation recommends kebab-case aliases (androidx-core-ktx) because the nested accessors give better autocompletion. Two naming rules will bite you if you forget them:

Also watch for prefix collisions. If you have both androidx-compose-ui and androidx-compose-ui-tooling, Gradle handles it (the shorter one is still addressable), but an alias that is a prefix of another can read confusingly. Consistent group-artifact naming avoids most surprises.

How do I use the catalog in build.gradle.kts?

Declare plugins once in the root build file with apply false, then apply them in modules without a version.

build.gradle.kts (root)

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.android.library) apply false
    alias(libs.plugins.kotlin.compose) apply false
}

app/build.gradle.kts

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.app"
    compileSdk = 36
    // ...
}

dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(libs.androidx.lifecycle.viewmodel.compose)

    val composeBom = platform(libs.androidx.compose.bom)
    implementation(composeBom)
    androidTestImplementation(composeBom)
    implementation(libs.bundles.compose)
    debugImplementation(libs.androidx.compose.ui.tooling)

    implementation(libs.bundles.room)
    ksp(libs.androidx.room.compiler)
}

Notice there is no org.jetbrains.kotlin.android plugin. From AGP 9.0, Kotlin support is built into the Android Gradle plugin and enabled by default, so the separate plugin is no longer applied (and applying it fails the build). If you are still on AGP 8.x, keep a kotlin-android entry and apply it as before. The Compose compiler plugin and KSP are still separate plugins on both. The ksp(...) line assumes you have added the KSP plugin to [plugins] and applied it.

On Gradle versions older than 8.1, the IDE flagged libs inside plugins {} and you had to add @Suppress("DSL_SCOPE_VIOLATION"). Modern Gradle does not need it, so delete those annotations when you upgrade.

Reading a version in a build script

Add compileSdk = "36" and minSdk = "24" to [versions]. Versions are lazy providers, so you call get():

android {
    compileSdk = libs.versions.compileSdk.get().toInt()
    defaultConfig {
        minSdk = libs.versions.minSdk.get().toInt()
    }
}

Putting compileSdk and minSdk in the catalog is worthwhile in multi-module projects. In a single-module app it is ceremony; a literal is clearer.

Can I use the catalog in buildSrc or convention plugins?

Yes, but not automatically. buildSrc and included builds such as build-logic are separate builds and do not inherit the main build’s catalog. Import the same file in their settings script:

build-logic/settings.gradle.kts

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

To compile convention plugins against AGP you need the plugin artifact as a library, so add an entry for it:

[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }

build-logic/convention/build.gradle.kts

plugins {
    `kotlin-dsl`
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
}

Inside the convention plugin’s Kotlin code, the generated libs accessors are not available. Use the untyped API instead:

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType

class AndroidRoomConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) = with(target) {
        val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
        dependencies {
            add("implementation", libs.findLibrary("androidx-room-runtime").get())
            add("ksp", libs.findLibrary("androidx-room-compiler").get())
        }
    }
}

This is the pattern Google’s Now in Android sample uses. If you are deciding whether you need convention plugins at all, the short answer is: not until you have three or more modules repeating the same android {} block. See modern Android app architecture for when modularization starts paying off.

How do I migrate from ext or buildSrc to a version catalog?

Do it in small steps. Each step leaves the build green.

  1. Create the file. Add gradle/libs.versions.toml with empty [versions], [libraries] and [plugins] sections. Sync.
  2. Move plugins first. Replace id("com.android.application") version "..." in the root script with alias(libs.plugins.android.application) apply false, and the module id(...) lines with alias(...). Plugins are few and the change is easy to verify.
  3. Move dependencies one group at a time. Add the entries, sync, then replace the strings. Doing AndroidX, then Compose, then networking keeps each diff reviewable.
  4. Delete the old source. Remove the ext {} block or the Deps.kt/Versions.kt objects once nothing references them.

From ext properties

Old projects often have this in the root script:

// Before (root build.gradle.kts)
extra["roomVersion"] = "2.6.1"

// Before (module)
val roomVersion: String by rootProject.extra
implementation("androidx.room:room-runtime:$roomVersion")

Each extra value becomes a [versions] entry and each string becomes a [libraries] entry. The win is immediate: the IDE now knows what libs.androidx.room.runtime is, while $roomVersion inside a string was invisible to it.

From buildSrc dependency objects

A buildSrc Deps object gave you type safety years before catalogs existed, so the question is whether switching is worth it. Usually yes:

Keep buildSrc (or better, a build-logic included build) for real build logic such as convention plugins, and move only the coordinates and versions into the catalog.

Rule of thumb: the catalog holds what you depend on; convention plugins hold how modules are configured. Do not try to encode build logic in TOML, and do not keep version strings in Kotlin.

Can I have more than one catalog?

Yes. Register extra catalogs in settings.gradle.kts:

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

Entries are then available as tools.someAlias. Each catalog can call from() once, and every catalog adds an extension to every project, so pick names that will not collide with existing extensions. Most apps should stick with one libs catalog; a second catalog is mainly useful for sharing a published catalog across many repositories.

How do I keep catalog versions up to date?

Android Studio highlights entries in libs.versions.toml that have a newer version available, and you can apply the update with a quick fix. For automated pull requests, both Dependabot and Renovate support the version catalog format. Whatever you use, upgrade AGP and Gradle together and read the compatibility table first; most of the errors in common Android Gradle build errors start with a catalog bump that moved one without the other.

Two habits keep the file maintainable:

Once your catalog is in place, choosing what goes in it is the next question. Our guide to essential Android libraries covers the defaults worth adding to a new app.

Frequently asked questions

Where does the version catalog file go?

Put it at gradle/libs.versions.toml in the root project. Gradle picks up that path automatically and exposes it as the libs extension, so you do not need to register it in settings.gradle.kts unless you want a second catalog or a different file name.

Does a version catalog force the versions it declares?

No. A catalog entry is a requested version, and Gradle's normal conflict resolution still runs, so a transitive dependency can pull in a newer one. Use a platform or dependency constraints, or a strictly rich version, if you need to enforce a version.

Should I put the Kotlin Android plugin in my version catalog?

Only if you are on AGP 8.x or older. From AGP 9.0 Kotlin compilation is built into the Android Gradle plugin, so you remove the org.jetbrains.kotlin.android entry; the Compose compiler plugin and KSP still need catalog entries.

Can I use the libs accessors inside a convention plugin's Kotlin code?

Not the generated type-safe ones. Import the catalog in the included build's settings file, then look entries up with extensions.getByType<VersionCatalogsExtension>().named("libs") and findLibrary("alias").

Is a version catalog worth it for a single-module app?

Yes, though the benefit is smaller. You still get one readable list of dependencies, IDE autocompletion and update tooling that understands the file, and new Android Studio projects already include one.