Efficient Dependency Management in Android

Photo by Firmbee.com on Unsplash

Efficient Dependency Management in Android

Unleashing the Power of Version Catalog

Every Android developer has worked on a monolith and managing dependencies for such a project is quite straightforward because you only get to deal with a single module. However, things start to get a bit tricky when you want to add another module to your project because of repeating the whole process of redeclaring the same dependencies in the other module.

This goes against the principle of "Don't Repeat Yourself(DRY)" which states that "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."

When we think of dependency management regarding this principle, we can apply it in several ways.

  1. Avoid having multiple versions of the same library in different parts of your project.

  2. Have a centralized place that contains all the dependency information.

  3. Having a system that automatically updates the application dependencies.

I won't delve into automatic updates of dependency versions because it's beyond the scope of this article but I will create a separate article that talks about that in detail.

So what is Version Catalog?

Well, It's simply a Gradle feature that was introduced in Gradle 7.0 that allows us to have all our dependencies in a centralized place.

Before the Version Catalog, most developers used to create a buildSrc directory which Gradle treats as an included build. Feel free to look at it at your convenience.

Let's go through the steps involved in creating a version catalog:

  • In a new/existing Android project, switch to the project view.

  • Look for the gradle folder, right-click, select New > File

  • Name the file: libs.versions.toml

You should be having something like this at the end

The Version Catalog structures the dependency details in sections as shown below

This is the current state of our dependencies after the initial project creation

dependencies {

    implementation("androidx.core:core-ktx:1.10.1")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
    implementation("androidx.activity:activity-compose:1.7.2")
    implementation(platform("androidx.compose:compose-bom:2023.03.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
}

Let's start moving the androidx-core-ktx dependency to the version catalog.

[versions]
androidx-core-ktx = "1.10.1"

[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }

This is how the dependency will look afterward

implementation(libs.androidx.core.ktx)

I've gone ahead and moved the rest of the dependencies to the version catalog.

dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.lifecycle.viewmodel.compose)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.compose.ui)
    implementation(libs.androidx.compose.ui.graphics)
    implementation(libs.androidx.compose.ui.tooling.preview)
    implementation(libs.androidx.compose.material3)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.compose.ui.test.junit4)
    debugImplementation(libs.androidx.compose.ui.tooling)
    debugImplementation(libs.androidx.compose.ui.test.manifest)
}

You can notice that we have a few common dependencies. We can add them to the bundles section in the Version Catalog as shown below.

[bundles]
implementation = ["androidx-core-ktx", "androidx-lifecycle-runtime-ktx", "androidx-lifecycle-viewmodel-compose", "androidx-activity-compose", "androidx-compose-ui", "androidx-compose-ui-graphics", "androidx-compose-ui-tooling-preview", "androidx-compose-material3"]
testImplementation = ["junit"]
androidTestImplementation = ["androidx-junit", "androidx-espresso-core", "androidx-compose-ui-test-junit4"]
debugImplementation = ["androidx-compose-ui-tooling", "androidx-compose-ui-test-manifest"]

Now our dependencies block looks a bit cleaner

dependencies {

    implementation(libs.bundles.implementation)
    implementation(platform(libs.androidx.compose.bom))
    testImplementation(libs.junit)
    androidTestImplementation(libs.bundles.androidTestImplementation)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    debugImplementation(libs.bundles.debugImplementation)
}

How about plugins? well here is how they will be added to the version catalog

[versions]
android-application = "8.1.0"
kotlin-android = "1.8.10"

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

The project-level Gradle file

// before
plugins {
    id("com.android.application") version "8.1.0" apply false
    id("org.jetbrains.kotlin.android") version "1.8.10" apply false
}

// after
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
}

The app-level gradle file

// before
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

// after
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
}

Conclusion

We have seen how working with the version catalog makes it easy for us to manage the libraries we add to our project. This will be even more beneficial as you start modularizing your project because you will notice how much boilerplate will be reduced. You will get even more benefits when you start considering using the Gradle Convention Plugins to share your build logic between submodules.

Happy Hacking!