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).

  1. 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.

  2. 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.

  3. 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.

  1. Creating the catalog in settings.gradle.kts file

  2. Creating a separate configuration file called libs.versions.toml in the gradle/ 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 just retrofit. The reason for it is, we are defining other aliases like retrofit-rx or retrofit-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, like retrofitRx or retrofitMoshi.

  • 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 in settings.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

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 ✌🏾