blog / gradle
Gradle Version Catalogs in Android: A Complete Guide
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:
- One place for versions. Bump Room once and every module that uses it moves together.
- Type-safe accessors. A typo in
libs.androidx.rooom.runtimefails at script compilation with a clear error, and Android Studio autocompletes entries. - A file tools can read. Dependabot, Renovate and Android Studio’s lint checks all understand the TOML format, so update tooling works without custom scripts.
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 alias | Kotlin DSL accessor |
|---|---|
androidx-core-ktx | libs.androidx.core.ktx |
androidx_room_runtime | libs.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:
- Reserved words such as
extensions,classandconventioncannot be used as aliases. bundles,versionsandpluginscannot be the first segment of an alias.versions-checkeris invalid;checker-versionsis fine.
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.
- Create the file. Add
gradle/libs.versions.tomlwith empty[versions],[libraries]and[plugins]sections. Sync. - Move plugins first. Replace
id("com.android.application") version "..."in the root script withalias(libs.plugins.android.application) apply false, and the moduleid(...)lines withalias(...). Plugins are few and the change is easy to verify. - 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.
- Delete the old source. Remove the
ext {}block or theDeps.kt/Versions.ktobjects 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:
- Any change to
buildSrcforces Gradle to rebuild it and recompile every build script, which on a large project is a noticeable hit on every version bump. - Update tools cannot parse Kotlin objects, but they parse TOML.
- The catalog is a standard Gradle feature, so new contributors already know it.
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:
- Sort sections alphabetically or by group and keep related libraries adjacent, so a diff shows exactly what moved.
- Remove dead entries. Unused aliases do not affect the build, but they mislead readers into thinking a library is in use. A quick search for the accessor before deleting is enough.
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.