If you are an Android developer with experience, you have gone through the different phases of using Gradle to manage dependencies in Android. 

First, it was done through Groovy. Easy to learn, not crazy different to Kotlin or Java, but it could be better.

Then, we jumped to the Kotlin Gradle DSL, and magically we could use the same language for our configuration files and for our base code.

Now, we have a new tool to manage dependencies, called version catalog, and the great thing is that it can be used with Groovy and with the Kotlin DSL.

Let’s see how to use it in a project, what are we going to gain using this tool, and what can be some of its disadvantages.

How to add it to a project?

The most used technique nowadays in my experience is to use the Gradle Kotlin DSL to manage dependencies by creating a module, usually called buildSrc. This module has different Kotlin objects for the names of the dependencies, the Gradle plugins and all the version numbers.

With the version catalog, we don’t need this module to have an organized set of dependencies.

The buildSrc module can be used for more things besides organizing your dependencies, so you may need to keep it anyway.

Now we just need a TOML file, named libs.versions.toml, that is going to be placed in the folder called gradle in the root of the project and to upgrade the Gradle version to at least 7.4 version, which can be done in the gradle-wrapper.properties file.

Writing the dependencies

To manage our dependencies, we are going to need three clear sections in the TOML file, versions, libraries, and plugins

This thing is pretty straightforward, as you can see in the next code snippet.

[versions]
activityCompose = "1.5.1"
androidxCore = "1.9.0"
androidxEspresso = "3.4.0"
androidGradlePlugin = "7.3.1"
appCompat = "1.5.1"
compose = "1.2.0"

[libraries]
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxEspresso" }
app-compat = { module = "androidx.appcompat:appcompat", version.ref = "appCompat" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
# Some other ktor dependencies that I'm skipping in this example to make it simpler

[bundles]
ktor = ["ktor", "ktor-serialization", "ktor-core", "ktor-cio", "ktor-content-negotiation", "ktor-json", "ktor-kotlinx-json", "ktor-logging", "ktor-okhttp", "ktor-client-android"]

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

TOML

You define your versions, your libraries associated to each version, and the same thing with plugins.

If you’ve read the snippet, you should have noticed the bundles section. This thing is great for libraries like Ktor or Compose that usually have a lot of different dependencies you may need. 

Thanks to it, when you want to use the dependency in the build.gradle.kts file, you will need to just add the bundle you’ve created, instead of all the dependencies one by one in every module that you need it to be.

Using the dependencies

After a Gradle sync, we can use this new version catalog in the build.gradle.kts file or the equivalent in Groovy. For these samples I’m going to use the Gradle Kotlin KTS as it’s what I’m more comfortable at the moment.

For the plugins, we have to do it like so.

@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
}
Kotlin

There are some things to talk about here. 

We defined our plugin dependencies using hyphens, but now we are using dots! This way you can access them like it were an object, and it’s going to suggest the libraries while you are writing, so no need to memorize them.

There’s also a bug that is going to raise a warning in the plugins section, but it’s actually fine, already reported and if nothing weird happens, going to be fixed.

To add a dependency to the module, we would do it like this.

dependencies {
implementation(libs.activity.compose)
api(libs.compose.ui)
}

It couldn’t be easier.

If you have a lot of dependencies in the same file, you can use the bundles we talked about before, or use the with keyword to avoid writing libs a thousand times.

Downsides

If you are thinking about migrating your existing project, this is going to be a boring process that’s going to take you some hours. 

It’s kind of repetitive writing every dependency or copy-pasting them, and for me and for many, this is the kind of task we want to avoid at all costs.

I’ve seen people using GitHub Copilot for this, and it did actually save them time, but at the end of the day, you are going to need to spend some hours with it, assuming everything goes as planned and being extra careful with the code Copilot suggests.

There is a plugin that in theory does this migration for you and keeps versions updated, but I haven’t tested it personally, so I can’t recommend it.

Another disadvantage could be that most of the current developers are educated on the traditional way of managing dependencies with Gradle. They are going to need to invest a bit of time understanding how the version catalog works, but you can share with them this article for that. ūüėČ

Advantages

Bundles are great! This is what I liked the most. Adding all the Jetpack Compose, Firebase or Ktor dependencies with just one line of code is beautiful.

In theory, if you only use the buildSrc module for dependency management, and you are able to remove it, compile times are going to be faster with the version catalog. I don’t believe this is going to be something that you can notice, but it’s something to think about.

As we said before, once you write libs, dependencies are going to be suggested to you, so no need to memorize the exact name of a dependency again, and you can also check if the dependency is already setup in the project without opening the TOML file.

Conclusions

Now that we know how to implement the version catalog, what are some of its downsides and advantages, we need to ask ourselves the question.

Is it worth it?

In my opinion, it depends. For new projects, absolutely yes! Migrating to the version catalog from a fresh project is going to take you only a few minutes (specially if you’ve done it before), and you are going to have a clean solution to manage your dependencies with direct support from Gradle.

For new projects, things are different. You have to take into account the time that is going to take the migration and see if members of your team would like to adopt this new approach.

And now to finish, here you have a project in GitHub made by Google themselves that uses the versions catalog if you need it.

Featured image by DALL·E 2


If you want to read more content like this and support me, don’t forget to check the rest of the bolg or subscribe here to get an email every time I publish new content.

Categorized in: