Android - Scalable dependency management with version catalogs
Tags: android, dependencymanagement, gradle, versioncatalogs
Published: 2023-12-13 21:56:58

I’m revisiting some helpful content for starting a new Android project. One of the pain points is how to handle a ton of dependency declarations for multi-module projects. Here is where version catalogs come to our rescue.

# Declaring dependencies

Before discussing the version catalogs, we will see how dependencies are being declared in an Android project. To declare a dependency in an Android project, you would add something like the following code in your `build.gradle.kts` file:

```kotlin
plugins {
  //...
}

android {
  //...
}

dependencies {
	// Here is where the dependencies are declared
	implementation("androidx.core:core-ktx:1.12.0")
	// ...other dependecies
}
```

## Coordinates

One important concept to have in mind is the concept of ***coordinates***. Each coordinate is composed by “**[group]:[artifact name]:[version**]”. For example, in the example above the coordinate of the `core-ktx` dependency is `[androidx.core]:[core-ktx]:[1.12.0]`, so the same dependency can be decomposed in a declaration like this.

```kotlin
implementation(group = "androidx.core", name = "core-ktx", version = "1.12.0")
```

Keep in mind this concept; we will return to it later.

## A common issue with multi-module projects

In case you have more than one module in your project, each dependency needs to be declared on every module that requires the specific dependency. The problem arises with this approach because of having repetitive declarations; every time you want to update a dependency version, you have to make the change in every gradle file where the dependency is declared. This makes dependency handling redundant and messy.

Here is where **Version catalogs** come to our rescue.

# Version catalogs

Version catalogs is a feature of Gradle, so you can use it if you use Gradle as your build system in your Android project. This allows you to declare and organize all your dependencies in a single file, which acts as a catalog where each dependency is associated to a single alias, making this catalogs acts as a source of truth and it can be referenced in every gradle file.

To enable this option you need to follow these steps:

1. Create a new file into the `gradle` folder at the project level with the name `libs.versions.toml`.
2. Declare the dependencies into that file.
3. Reference these declarations in every gradle file of each module.

If you don't know where the gradle folder is, you can typically find it at the root project level; you can use the Project view to look at it.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/je05000htzcgzulteiwa.jpeg)

At this point, you can ask… how these declarations must be made into that file?

# Basic usage

Now, let's see the structure of the `libs.versions.toml` file. 

```toml
[versions]
core-ktx = "1.12.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
```

In the most basic format you have to declare two sections `[versions]` and `[libraries]`, the first one is used to declare versions for each dependency, the second is for the dependencies itself.

For this example, we took the same dependency that it has been declared in the first example of this article, in the gradle file. In order to make the `core-ktx` dependency available into the libs catalog, we associate the alias `androidx-core-ktx` to the GAV (group, artifact, version) coordinates.

**The main benefit is these declarations are visible by all the modules in your project. So you can reference these declarations from your gradle file.**

```kotlin
plugins {
  //...
}

android {
  //...
}

dependencies {
	// Here is where the dependencies are declared
	implementation(libs.androidx.core.ktx)
	// ...other dependecies
}
```

*Note: Every time you added a new declaration in the TOML file you have to sync the project to make it available for the modules.*

Also, using Version catalogs let you to reference the same version for multiple different libraries, for example:

```toml
[versions]
android-paging = "3.2.1"

[libraries]
android-paging-common = { module = "androidx.paging:paging-common-ktx", version.ref = "android-paging" }
android-paging-runtime = { module = "androidx.paging:paging-runtime-ktx", version.ref = "android-paging" }
android-paging-rxjava2 = { module = "androidx.paging:paging-rxjava2-ktx", version.ref = "android-paging" }
```

Here, we simplify the coordinates using the module to group the `[group]:[artifact]` pair and use the same version reference for all the android-paging dependencies.

# Going a bit further

Not only you can declare the regular dependencies in that TOML file, you can declare your plugins and even create bundles for grouping dependencies.

## How to declare plugins

To declare a plugin you only need to add the `[plugins]` section.

```toml
[versions]
//Here the versions are declared
android-gradle-plugin = "8.2.0"

[libraries]
//Here the libraries are declared

[plugins]
android-application = {id = "com.android.application", version.ref = "android-gradle-plugin"}
```

So, after syncing the project, we can change the references from this

```kotlin
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id("com.android.application") version "8.2.0" apply false
    //... more plugins are placed here
}
```

To this

```kotlin
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    alias(libs.plugins.android.application) apply false
    //... more plugins are placed here
}
```

## How to create your own bundle

To create a bundle, you have to add a `[bundles]` section into your TOML file. For example, here I’m adding the Room bundle.

```toml
[versions]
room = "2.6.1"
ksp = "1.9.0-1.0.13"

[libraries]
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }

[plugins]
kotlin-symbol-processing = { id = "com.google.devtools.ksp", version.ref = "ksp" }

[bundles]
android-room-bundle = ["androidx-room-ktx", "androidx-room-runtime"]
```

Again, you can use the bundle like this after syncing your project.

```kotlin
plugins {
    // More plugins are added here...
		alias(libs.plugins.kotlin.symbol.processing)
}

android {
  //...
}

dependencies {
    // Some dependencies are declared here
    implementation(libs.bundles.android.room.bundle)
    ksp(libs.androidx.room.compiler)
}

```

# Conclusion

It’s also worth mentioning that using a version catalog is likely the best way to establish a single source of truth for all your project dependencies. It's also worth mentioning that the official documentation has a section about this topic.

You can experiment with different strategies using bundles to organize the necessary dependencies for each project, avoiding the tedious process of checking each Gradle file to see what you’ve declared.

## References

Migrate to version catalogs

https://developer.android.com/build/migrate-to-catalogs

Sharing dependency version between projects

https://docs.gradle.org/current/userguide/platforms.html