As projects start to be more complex and modularized, keeping all the dependencies in sync and updating them becomes a hard and annoying task. To solve this, we have a few alternatives and a new one was recently launched by Gradle to make our lives easier.
Gradle introduced in version 7.0 a new feature called Version Catalog. It represents a list of type-safe dependencies to be used across the projects. It is also very flexible and takes advantage of what Gradle currently has to offer. In this article, we will take a look at why using, how to apply it in an Android project and some points that are important to know before diving in.
Why Version Catalog?
The Version Catalog is a very flexible solution that takes advantage of existing Gradle features and allows us to do even more than the alternatives. One example is the ability to create bundles
which allow adding a single implementation()
line with a set of libraries in our Gradle File.
Version Catalog files are shareable, so it’s even easier to have a standard configuration not only inside it but across multiple projects. The flexibility also supports third-party plugins to automatically update the versions for the latest ones, if we want.
Also, in addition to composite builds, it performs better in comparison with the buildSrc
solution. For instance with buildSrc
, when we increment a version number the build is cleaned and needs to be rebuilt. This is not the case for Version Catalog. For more details about performance, please take a look at this great article by
. For more insights, this is a great Twitter thread to follow.
Basic configuration
In order to configure the Version Catalog in our project, a set of simple steps are required.
1. Create the libs.versions.toml file
The libs.versions.toml
file is the file that contains all the dependency definitions, such as versions
, libraries
, bundles
and plugins
definitions.
[versions] | |
# Define the dependency versions | |
kotlin = "1.7.10" | |
compose = "1.2.1" | |
[libraries] | |
# Define the libraries | |
compose_ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } | |
compose_material = { module = "androidx.compose.material:material", version.ref = "compose" } | |
compose_tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } | |
compose_icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } | |
[bundles] | |
# Define bundles/groups of libraries | |
compose = ["compose.ui", "compose.material", "compose.tooling", "compose.icons"] | |
[plugins] | |
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } |
It’s worth mentioning that all the dependency alias are normalized by Gradle. It means that every alias that has -
, _
or.
will be updated to use .
instead. For instance the alias compose_ui
, compose-ui
or compose.ui
will be normalized as compose.ui
.
Keep in mind that it’s also possible to define the Version Catalogs in the settings.gradle(.kts)
, however for clarity we are showing only with the TOML file. For more information about this and other ways to define your dependencies, please take a look at the official docs.
2. Create a dedicated module for plugins
Since we are using composite builds with Version Catalog, we need a dedicated module. The module does not need a specific name, but just make sure that it won’t conflict with the existing ones in your project. In our example, let’s call it plugins
.
This module needs to contain at least two files: a build.gradle(.kts)
and settings.gradle(.kts)
. The first one is used for regular Gradle configuration and the other is used to link up our libs.versions.toml
file with the Version Catalog feature.
plugins { | |
`kotlin-dsl` | |
} | |
repositories { | |
mavenCentral() | |
google() | |
} |
dependencyResolutionManagement { | |
repositories { | |
mavenCentral() | |
} | |
versionCatalogs { | |
create("libs") { | |
from(files("../gradle/libs.versions.toml")) | |
} | |
} | |
} |
3. Setup Composite Build
Now that we have our Version Catalog set, we need to set up the composite build to link this configuration with our project. In order to achieve that, we need to open our project’s settings.gradle
file and include a few lines:
pluginManagement { | |
includeBuild("plugins") | |
repositories { | |
gradlePluginPortal() | |
google() | |
mavenCentral() | |
} | |
} |
The code above sets the plugin management to not only look for dependencies in the remote repositories but also search for them in our plugins
module.
If your project is using a Gradle version below 7.2
, you will need to add a new feature preview activation on the settings.gradle
file.
enableFeaturePreview("VERSION_CATALOGS") |
4. Use the Version Catalog
Finally, after this setup, we are able to use the dependencies in our Gradle files.
dependencies { | |
// Adds a single dependency | |
implementation libs.kotlin | |
// Add a group of dependencies | |
implementation libs.bundles.compose | |
// Tests dependencies works in the same way | |
testImplementation libs.junit | |
androidTestImplementation libs.espresso.core | |
} |
Good to know
In addition to the points already mentioned about Version Catalog, there are a couple of things that are good to know.
Automatically update versions
One of the advantages of the Version Catalog is the ability to use tools to automatically update them. For projects on GitHub, RenovateBot is a great tool to integrate into your pipeline. This bot reads your libs.versions.toml
and automatically creates Pull Requests to update your dependencies.
My pipeline has a set of unit and instrumented tests, so I’m confident that the new version is compatible and I just click the merge button. If the tests fail, I just update the PR with the API changes and it’s good to go.
Job Offers
However, if your project does not support that plugin, a good alternative is the Version Catalog Update Plugin to directly apply to the project’s Gradle file. Both plugins are widely configurable and help us keep the project dependencies fresh.
No support for precompiled script plugins
One of the points of attention from Version Catalog is that the dependencies and versions are not visible in precompiled script plugins. In other words, the dependencies declared on libs.versions.toml
will be visible on the build.gradle
from all your modules, but not for custom/precompiled ones.
Let’s say that we have dozens of library
modules on our project and want all of them to have the same basic dependencies. We could create a library_dependencies.gradle
containing them all and just apply it to the appropriate modules.
plugins { | |
id("com.android.library") | |
} | |
dependencies { | |
// Won't be able to find this library | |
implementation(libs.android.appcompat) | |
} |
Unfortunately, this new Gradle file won’t be able to find the appropriate dependency due to some limitations. One way to workaround this issue is to create an extension function to manually expose the dependency.
internal val VersionCatalog.logcat: Provider<MinimalExternalModuleDependency> | |
get() = getLibrary("logcat") | |
private fun VersionCatalog.getLibrary(library: String) = findLibrary(library).get() |
Now instead of trying to directly access the Version Catalog, we manually access the libs
reference and the implementation
function will access our extension function and work properly.
plugins { | |
id("com.android.library") | |
} | |
// Manually get the reference for "libs" | |
val libs: VersionCatalog = | |
extensions.getByType<VersionCatalogsExtension>().named("libs") | |
dependencies { | |
// Use the extension function to access the dependency | |
implementation(libs.android.appcompat) | |
} |
One of the drawbacks of this solution is that it’s type unsafe since we are using Strings to find the dependencies rather than generated code. For more information about this behaviour, I highly recommend this GitHub Issue with an extensive discussion about this topic.
Conclusion
The Version Catalog is a great new way to handle dependency management on our projects. It makes the maintenance of both libraries and versions easier and also allows all the flexibility that Gradle has to offer. Some Android projects already started migrating and we will start to see more and more code, examples and improvements on this amazing feature.
What’s next?
As usual, I would love to share more complex code to help you in your studies. The first Pull Request it’s on my personal project, Alkaa, which migrates from buildSrc
to Version Catalog. It also adds a few JVM Convention Plugins to help avoid duplication, but this is a topic for the next article. 🙂
Also, there are two PRs from amazing projects that I used a lot as examples when I was doing the migration. The first one is ShoppingApp by
. A huge thanks to both for their amazing contributions! ❤️
The code used in this article is available on GitHub, where you can see step by step each part of the process:
The official Gradle documentation also has great examples and insights on how to improve dependency management on our projects. For more deep and informal content, this article from Cédric Champeau is great. And last but not least, the official Now In Android by Google also uses the Version Catalog.
Thanks a lot for reading my article! ❤️
Thanks to Bruno Kenji Tiba
This article was originally published on proandroiddev.com on August 25, 2022