Photo by Firmbee.com on Unsplash
Efficient Dependency Management in Android
Unleashing the Power of Version Catalog
Table of contents
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.
Avoid having multiple versions of the same library in different parts of your project.
Have a centralized place that contains all the dependency information.
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!