How to Clean Up Your Android App's Dependencies: The Ultimate Guide

Tech Mar 14, 2023

We recently started modularising our Android app and realised the need for better dependency management as we scale. We didn't want to repeat the same set of dependencies with different versions in different modules. So, we began the search for the perfect solution.

This blog post will help you understand different Gradle dependency management approaches, highlighting their evolution over the past few years. Feel free to skip to the end to know the approach we finally chose.

Let’s dive into some of these approaches.

1) Dependencies in build.gradle files

This is the standard approach for managing dependencies in Android, which is auto-applied when creating a project. While we have been using it so far, we are currently working to transition away from it. Nonetheless, we wanted to acknowledge its longstanding presence and significance.

https://media.giphy.com/media/OkzCcGn5fY29e7bvpS/giphy.gif

In this approach, we add dependencies directly inside the build.gradle of each module or project.

build.gradle

dependencies {
    implementation "androidx.core:core-ktx:1.9.0"
    implementation 'androidx.activity:activity:1.6.1'
    implementation 'androidx.activity:activity-ktx:1.6.1'
}

The pros

  • No setup is required; everything is done by default
  • Getting notified when a new update is available

The cons

  • Difficult to keep track of and manage versions
  • No navigation support
  • No auto-completion support
  • Does not provide any indication or highlight errors
  • Issues with versioning
  • Redundant code

2) ExtraPropertiesExtension (ext)

In our search for the perfect solution, we encountered a widely used approach that was popular a few years ago. This involves creating a separate Gradle dependency file and utilizing Gradle's "ExtraPropertiesExtension" to access dependencies.

The following code snippets illustrate the implementation of the approach:

  • A separate Gradle file is created to manage the dependencies
  • The dependencies are then applied to the project's Gradle file
  • Finally, the module's Gradle files can access these dependencies

config.gradle

def libraries = [:]
supportAppCompat: "com.android.support:appcompat-v7:$versions.supportLibs"

or

ext.libraries = [
supportAppCompat: "com.android.support:appcompat-v7:$versions.supportLibs"
]

***build.gradle (*project)

apply from: 'config.gradle'

build.gradle (module)

dependencies {
    compile libraries.supportAppCompat
}

The pros

  • Eliminates the need to repeatedly rewrite the same dependency code in each module
  • Prevents issues with versioning
  • Notifies when a new update is available

The cons

  • Requires familiarity with Groovy
  • Lacks navigation support, which can hinder development efficiency
  • Lacks auto-completion support, making dependency management more time-consuming
  • Does not provide error highlighting, which can make troubleshooting difficult
https://media.giphy.com/media/PnggNmuamz7kbgfUTL/giphy.gif

To learn more about this approach or about ExtraPropertiesExtensions in general, you can refer to this Gradle documentation.

https://docs.gradle.org/current/dsl/org.gradle.api.plugins.ExtraPropertiesExtension.html

3) BuildSrc + Kotlin DSL

This approach is particularly interesting as it is significantly advanced compared to previous methods, particularly due to its utilisation of Kotlin DSL and accompanying IDE support.

https://i.embed.ly/1/display/resize?url=https%3A%2F%2Fi.giphy.com%2Fmedia%2FmG1uA7JnuAZqbWV44g%2Fgiphy.gif&key=a19fcc184b9711e1b4764040d3dc5c07

What is Kotlin DSL?

Many developers are already familiar with Kotlin DSL, which offers a flexible tool for leveraging the power of Kotlin programming language.

Gradle’s Kotlin DSL provides an alternative syntax to the traditional Groovy DSL, along with an improved editing experience in supported IDEs. This includes enhanced content assist, refactoring, documentation, and more.

What is BuildSrc?

The BuildSrc directory, located in the root of a Gradle project, contains custom build logic written using the Kotlin DSL. With minimal setup requirements, this logic can be shared across the entire project. The following code snippet demonstrates this concept.

Create a buildSrc folder and enable Kotlin DSL

buildSrc/build.gradle.kts

import org.gradle.kotlin.dsl.`kotlin-dsl`

plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

buildSrc/src/main/kotlin/Dependencies.kt

object Libs {
   val androidxCamera2 = "androidx.camera:camera-camera2:1.1.0"
}

build.gradle

dependencies {
    implementation(Dependencies.androidxCamera2)
}

Initially, we were excited and felt like we had found the perfect solution. However, our enthusiasm was short-lived when we discovered a significant trade-off associated with this approach.

According to Gradle docs

Any changes made to buildSrc will cause the entire project to become out-of-date. As a result, when making small incremental changes, utilising the --no-rebuild command line option can provide faster feedback. However, it is important to run a full build regularly, or at least once development is complete.

The pros

  • Eliminates the need to rewrite dependencies repeatedly for each module
  • Eliminates versioning issues
  • Provides Kotlin support
  • Eliminates the need to learn Groovy
  • Offers navigation support
  • Provides auto-completion support

The cons

  • Any change to a dependency will trigger recompilation of build scripts and invalidate the Gradle cache, causing rebuilding of more than what is needed for a single version change
  • It has poor performance when compared to Groovy scripts, as documented by Gradle, android, and a reported open ticket on GitHub
  • Increased build time
  • The remote Gradle cache also gets invalidated
  • Automatic version checks do not work

Anything that affects the build time is a huge concern, so we decided to move forward. However, the decision to use this approach may vary depending on the project. For instance, in a single module project, it might work just fine.

*Note: ***There is an open ticket on Github. This has a pull request that proposes changes to make buildSrc work like an implicit plugin included in a build (that is, an included build declared via pluginManagement { includeBuild(x) }). This means that if you choose to use this approach, it's possible that the performance will significantly improve in the future. However, it's up to you to decide whether to proceed with this approach.

4) Composite builds and Kotlin DSL

This method uses composite builds to address the issues with the buildSrc approach and assumes that it may take some time before Gradle incorporates the changes to make buildSrc function like an included build.

https://media.giphy.com/media/mMDA2ZvTdzJPk6RAlP/giphy.gif

In this method, we create custom plugins that define dependencies, and then we apply these custom plugins in the settings.gradle file for use in the build. i.e we move our logic from buildSrc into a separate plugins module and add includeBuild(“plugins”) within settings.gradle. Voila!

plugins/src/dependencies.kt

class Dependencies : Plugin<Project> {
  override fun apply(project: Project)
    {
     companion object
        {
          val androidxCamera2 = "androidx.camera:camera-camera2:1.1.0"
        }
    }
}

settings.gradle

includeBuild("plugins")

The pros

  • Avoids the need to rewrite dependencies repeatedly for each module
  • No need to have a deep understanding of Groovy
  • Support for Kotlin DSL
  • Changes do not invalidate the Gradle cache
  • Navigation support for easy code browsing
  • Auto-completion support for faster development

The cons

  • There is no standardised approach for achieving this
  • The setup process is more complex than other approaches
  • Automatic version checks do not work with this
  • It has poor performance when compared to Groovy scripts. Since most documents mention this, you can refer to the following links - gradle, android, and an open ticket on GitHub.

You can read more about composite builds here.

5) Version Catalog

Finally, we came across the solution we were looking for - Version Catalog by Gradle.

Gradle's Version Catalog, introduced in version 7.0, is a powerful tool for managing dependencies across multiple projects. It provides a centralised list of type-safe dependencies to be used across projects. The Version Catalog is flexible and customisable. Overall, the Version Catalog is a valuable addition to the Gradle toolchain and worth exploring for better dependency management.

https://i.embed.ly/1/display/resize?url=https%3A%2F%2Fi.giphy.com%2Fmedia%2F1ym5LJ17vp77BL8X5O%2Fgiphy.gif&key=a19fcc184b9711e1b4764040d3dc5c07

The Version Catalog is a file added to the Gradle folder in the root directory of your project, using the TOML configuration format. While there are other ways to declare dependencies in Gradle without using the TOML file, using the Version Catalog with a TOML file can help to centralise and simplify your dependency management.

The TOML file will look something like this.

gradle/libs.versions.toml

[versions]
androidxActivity = "1.6.1"
androidxCamera = "1.1.0"
androidxCompose-runtime = "1.3.1"

[libraries]
androidxActivity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
androidxActivityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidxActivityKtx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }

[plugins]
android = "android:7.3.1"
androidLibrary = "android-library:7.3.1"

[bundles]
testDependencies = ["junit-jupiter", "junit-engine"]

build.gradle

dependencies {
implementation libs.androidxActivityCompose
}

That’s it!

Why did we decide to go ahead with Version Catalogs?

  • Standardisation and performance
  • You can declare a group of dependencies using a single line in any module by using Bundles
  • Supported and recommended by Gradle
  • Stable release is out
  • The setup is simple
  • Android Giraffe allows easy navigation from Gradle files to the TOML file with Version Catalog
  • Autocompletion is available on Android Giraffe
https://cdn-images-1.medium.com/max/1600/1*ivqtkW5FXIGYLERe1CYl8w.png
  • Highlight (Android Giraffe onwards)
https://cdn-images-1.medium.com/max/1600/1*VeBwKIh6vDHW-N0GaLrxow.png
  • In Android Giraffe, you can edit variables you’ve defined there through the Project Structure dialog Variables view (File > Project Structure > Variables) in Android Studio
https://cdn-images-1.medium.com/max/1600/1*KhvurcmxU7yMLQavCZm4xw.png

Challenges you may face

  • Migration might get hectic, especially for larger projects
  • Getting accustomed to a TOML file
  • The need to use Platform to control transitive versions as is mentioned specifically by Gradle
  • Dependabot support is not yet available, but possible in the near future, creating the need to get familiar with third-party libraries like RenovateBot for now
  • Possible bugs are on the way since it is still in its early stages, but stable
  • AutoComplete and Navigation features are not yet supported in stable Android Studio releases. To access these features, you need to install the latest Android Studio Griffin, which is a Canary version.

Who’s using Version Catalogs?

We found that some renowned companies such as Google, Square, Airbnb, Pinterest, CashApp, and Ktor are already using Version Catalogs in their projects. Some repositories that use Version Catalogs include:

Conclusion

Each approach we discussed has its tradeoffs in terms of automatic version updates, Kotlin DSL, standardisation, performance, and more. Ultimately, the best approach will depend on your project's specific requirements. Version Catalog seems to be a promising option as it's recommended and created by Gradle, has upcoming new features and optimisations, is simple to set up, and has IDE support from Android Studio. With reputed projects already adopting it, Version Catalog seems to have no significant pitfalls. However, choosing the right approach depends on your project and the tradeoffs you're willing to accept.

To learn more about implementing Version Catalogs and advanced topics related to it, you can refer to this Documentation on how to get started.
Happy coding!

Written by Pranav Jayaraj

Tags