Gradle version catalog for better dependency management
There are few different approaches for managing dependencies, these are generally used approaches (at least on the Android development side).
-
Manual management: This is the basic approach that we get when we create a project, while this is good enough for most single module projects. It gets annoying to manage dependencies either for version updates or adding/removing as the projects grow and adds more modules.
-
Gradle extra property: This approach is quite popular with a lot of developers and you can find it being used in a lot of open-source projects and Google recommends this approach as well in their docs. With this approach, you can share the dependencies or versions or both. It’s a step up from manual dependency management.
-
Gradle buildSrc & Kotlin DSL: This approach became quite popular with a lot of developers recently and even the Android team uses this approach in the Jetpack libraries. In this approach you get the benefit of sharing your dependencies as well as using Kotlin for Gradle build files, this approach provides you with auto-completion of the versions/dependencies in your Gradle files. One downside of this approach is, if you’re just changing versions in the config file, Gradle will rebuild the entire
buildSrc
which includes plugins you defined.
You can refer this article for more information on these approaches: Kotlin + buildSrc for Better Gradle Dependency Management – Handstand Sam
Gradle Version Catalog
Version catalog is a preview feature that is available in Gradle 7.0.0. This feature allows us to define our dependencies and versions in a separate configuration file (or in the settings.gradle.kts
file) and then Gradle will generate type-safe accessors that can be used in the build scripts with auto-completion in IDE.
A version catalog is a list of dependencies, represented as dependency coordinates, that a user can pick from when declaring dependencies in a build script.
I will be using Gradle Kotlin DSL for examples, but this can be used with Groovy as well
Here is a small example of how the generated type-safe accessor will look like.
dependencies {
implementation(libs.androidx.appcompat)
}
In this above example, libs
is the catalog that is generated (you can configure the name, more on that later) and androidx.appcompat
is the dependency. These generated references can be used in build scripts across the project and also can be shared across the org for different projects.
So obviously this seems like something the other approaches would provide, now let’s look at one of the great advantages of using the catalog. Version catalog provides us with a way to define bundles, which are a group of dependencies. Essentially you can now bundle different dependencies that are commonly used together and define them as a single dependency in your build script.
Let’s take Retrofit for example, when declaring the Retrofit dependency we usually end up adding additional dependencies to add additional support such as Moshi, Interceptors, Rx support etc., So instead of declaring the dependencies individually
implementation(libs.retrofit.base)
implementation(libs.retrofit.moshi)
implementation(libs.retrofit.rx)
While this doesn’t seem bad, imagine a mid/large scale project where we have a bunch of these similar dependencies in our build scripts.
Now with bundles, we can define the bundle with dependencies in our config file and then reference that bundle in our build script.
implementation(libs.bundles.retrofit)
So instead of 3 different dependencies, we have a single dependency that can be declared in our build scripts 🤯. Adding a single bundle in the implementation
is the same as defining them individually.
✨ Setup ✨
Now let’s look at how to set up a version catalog in your app. Before we can create a dependency configuration file and start using the generated accessors, we need to first enable the version catalog feature, since it’s in feature preview. To enable it just add this to your settings.gradle.kts
file
enableFeaturePreview("VERSION_CATALOGS")
Once you run the Gradle sync, the feature is enabled. Now we can start adding the catalog. There are 2 approaches we can take for defining the catalog.
-
Creating the catalog in
settings.gradle.kts
file -
Creating a separate configuration file called
libs.versions.toml
in thegradle/
subdirectory.
Creating the catalog in settings.gradle.kts
file
One of the easiest ways to try out this feature is to define the version catalog in the settings.gradle.kts
file. Let’s consider the above retrofit example, for the retrofit dependencies to be available, we need to associate an alias with the GAV (group, artifact, version) coordinates.
# version catalog in `settings.gradle.kts`
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
alias("retrofit-base").to("com.squareup.retrofit2:retrofit:2.9.0")
alias("retrofit-moshi").to("com.squareup.retrofit2:converter-moshi:2.9.0")
alias("retrofit-rx").to("com.squareup.retrofit2:adapter-rxjava2:2.9.0")
}
}
}
This would generate our type-safe accessors for each subgroup, like so libs.retrofit.base
, libs.retrofit.moshi
, libs.retrofit.rx
. Since we have used the name “libs” for creating the version catalog the extension is available by the name libs
in our build scripts. But we can change that name here and have a different one like deps
, dependencies
, etc.,
As you can see in the above example, we are using the same version for Retrofit. Instead of repeating it, we can declare the version and reference it.
# referencing same version
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
version("retrofit", "2.9.0")
alias("retrofit-base").to("com.squareup.retrofit2", "retrofit").versionRef("retrofit")
alias("retrofit-moshi").to("com.squareup.retrofit2", "converter-moshi").versionRef("retrofit")
alias("retrofit-rx").to("com.squareup.retrofit2", "adapter-rxjava2").versionRef("retrofit")
}
}
}
Type-safe accessors are also available for the versions that are defined separately. For example, you can get the retrofit version like so if you want.
# getting retrofit version
libs.versions.retrofit.get()
Now let’s take a look at how to define bundles, it’s straightforward. We already have everything we need in place, so we just need to pass the aliases to the bundle
.
# defining bundle
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
version("retrofit", "2.9.0")
alias("retrofit-base").to("com.squareup.retrofit2", "retrofit").versionRef("retrofit")
alias("retrofit-moshi").to("com.squareup.retrofit2", "converter-moshi").versionRef("retrofit")
alias("retrofit-rx").to("com.squareup.retrofit2", "adapter-rxjava2").versionRef("retrofit")
bundle("retrofit", listOf("retrofit-base", "retrofit-moshi", "retrofit-rx"))
}
}
}
That’s it, now we can reference our bundle in the build script, by calling libs.bundles.retrofit
.
libs.versions.toml
file
If you’re like me and don’t want to add dependencies in your settings.gradle.kts
file, Gradle provides another approach. You can create a libs.versions.toml
file in your gradle/
subdirectory in your project.
Wait, hold up. What’s is that TOML format? you might ask. It’s a configuration file format that is minimal and easy to understand, you can read more about it here.
Once we added the libs.versions.toml
file, Gradle will automatically pick that as input to the libs
catalog. In case you want to change the name. In settings.gradle.kts
you can change the default name
# Changing default libraries extension name
dependencyResolutionManagement {
defaultLibrariesExtensionName.set("dependencies")
}
Now that we have our configuration file setup, let’s look at the options it provides. Well, there isn’t much, it has 3 sections we can use
-
The [versions] section is used to declare versions that can be referenced by dependencies
-
The [libraries] section is used to declare the aliases to coordinates
-
The [bundles] section is used to declare dependency bundles
Let’s take our catalog defined in settings.gradle.kts
and declare it in the configuration file.
# libs.versions.toml example
[versions]
retrofit = "2.9.0"
[libraries]
retrofit-base = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-rx = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofit" }
retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
[bundles]
retrofit = ["retrofit-retrofit", "retrofit-rx", "retrofit-moshi"]
That’s it, we have our configuration file and we can start referencing the generated type-safe accessors in our build scripts.
Personally, I like this approach, this kind of configuration file allows us to share across org if required, gives a quick overview of all the dependencies (excluding transitive dependencies) in the project. But feel free to use whatever approach you find comfortable with.
Gotchas
So far we have talked about how amazing and useful the version catalog is. Now let’s take a step back and see what are some of the gotchas when using this feature.
-
Preview Feature: Let’s get this out of the way first, this still a preview feature, so it’s subjected to change and may also be unreliable in certain cases. So use it at your own risk.
-
Alias naming: When I was first started exploring this feature and started creating aliases, I quickly ran into build errors when defining alias names. Later I learn that it’s illegal to have an alias to dependency which also belongs to a nested group. For example, if you look at our retrofit declarations we used the alias
retrofit-base
instead of justretrofit
. The reason for it is, we are defining other aliases likeretrofit-rx
orretrofit-moshi
, so Gradle has to generate sub group accessors for them as well. One of the recommended approaches to resolve this issue is to have a different name, but you can also switch cases when defining sub groups, likeretrofitRx
orretrofitMoshi
. -
Plugin management: At the time of writing this article there isn’t a way to define plugins in the catalog. But there is a proposal open for this feature and the Gradle team is working on it, so we may see it in future. For now we can use
pluginManagement
API insettings.gradle.kts
pluginManagement {
plugins {
id("com.diffplug.spotless") version "5.14.1"
}
}
- IDE Support: At the time of writing the current stable Android Studio (v4.2.2) shows a
MISSING_DEPENDENCY_CLASS
error in build scripts, to resolve that we have to update to Android Studio Arctic Fox or above. (For IntelliJ Idea users update to IDEA 2021.1 or above). While it shows this error in the editor, everything complies and syncs fine.
The editor doesn’t show update suggestions for dependencies as well, not even in the project structure dialog (at least in Android Studio). But you can use 3rd party plugins or CLI tools to resolve this issue.
Conclusion
Version catalog is an improvement from the existing approaches. Bundles provide a really good way to define commonly used dependencies in the build scripts. It’s faster to build compared to the buildSrc
approach I was using before, which offers type safety and auto-completion. Overall I am happy to use this approach in my projects going forward.
Resources
-
Sharing Versions - Gradle Docs
-
Frequently asked questions about Version Catalog - Cédric Champeau
If you want to discuss more or if I got anything wrong, feel free to reach to me via email or on Twitter. Until next time ✌🏾