https://d226lax1qjow5r.cloudfront.net/blog/blogposts/the-right-way-of-using-gradle-dynamic-dependencies/blog_gradle-dynamic-dependencies_1200x600.png

The Right Way of Using Gradle Dynamic Dependencies

Published on November 4, 2020

This article was updated in April 2025

Gradle is a powerful and flexible build system widely used in Android development and other Java-based projects. It offers dependency management, allowing you to define external libraries and frameworks your application will rely on.

Defining Dependencies in Gradle

Typically, you define dependencies in Gradle like this:

dependencies {

    implementation 'com.vonage.client:client-sdk:2.7.0'

}

In the example above, Gradle downloads the specific version 2.7.0 of the Vonage Client SDK. However, when you want Gradle to manage library versions dynamically, you can use a dynamic version instead of specifying a fixed version number.

Understanding Semantic Versioning

Most libraries today follow Semantic Versioning (semver), which uses a major.minor.patch format (e.g., 2.7.0). The versioning rules are as follows:

  • Major: Incremented when there are backward-incompatible changes in the API (e.g., method signature changes).

  • Minor: Incremented when new functionality is added in a backward-compatible manner (e.g., adding new methods).

  • Patch: Incremented for backward-compatible bug fixes (e.g., fixing bugs without introducing new features).

Using Dynamic Dependencies

Gradle lets you use dynamic versioning, allowing it to automatically fetch the latest compatible version of a dependency. Here's how you can define a dynamic version:

dependencies {

    implementation 'com.vonage.client:client-sdk:2.7.+'

}

In the above example, Gradle will fetch the most recent patch version of 2.7.x (e.g., 2.7.1, 2.7.2). You can use other forms of dynamic dependencies, such as:

  • implementation 'com.vonage.client:client-sdk:2.+': This fetches the latest version within the 2.x range (both minor and patch versions).

  • implementation 'com.vonage.client:client-sdk:+': This fetches the latest available version of the library.

While dynamic dependencies help make sure that you’re always using the latest bug fixes and improvements, they come with some major caveats.

Why You Should Be Careful with Dynamic Dependencies

Dynamic dependencies can lead to unpredictable behavior. Below are some potential problems and scenarios where dynamic dependencies cause issues.

Problematic Scenario 1: Inconsistent Builds

Imagine your app was working fine a month ago, but after a bug was introduced, it stopped functioning as expected. You check out the repository from that time, rebuild the app, but the issue still persists. What happened?

The problem is that the dynamic dependency mechanism in Gradle fetched the latest version of the external library, which now contains the bug. Even though your source code was from a month ago, the external library version wasn’t locked, so the latest version was downloaded, introducing the problem.

Lesson 1: Rebuilding your app with dynamic dependencies can be unreliable because the dependencies are updated over time, making it difficult to reproduce the exact environment your app was built on.

Problematic Scenario 2: Unpredictable Bugs for Users

If you’re a library creator and recommend users to use a dynamic version of your library (e.g., com.vonage.client:client-sdk:2.7.+), they might unknowingly encounter bugs introduced in new releases of the library. They’ll likely report issues, but without knowing the exact cause—because they were using a newer version than expected.

Lesson: Avoid advising users to use dynamic dependencies for production applications. It can be hard to pinpoint the cause of a bug, and users might not be able to easily revert to a working version.

Modern Solutions for Managing Dependencies

To mitigate the issues caused by dynamic dependencies, modern tools and strategies are available:

1. Use Gradle Locking Features

Gradle’s built-in dependency locking allows you to control dependency versions even when using dynamic versioning. This ensures that, while the version specifier in the build.gradle file can be dynamic, the actual version used during builds is locked down.

To enable dependency locking, you can add the following to your build.gradle file:

dependencyLocking {
    lockAllConfigurations()
}

Then, to generate a lock file, run:

./gradlew dependencies --write-locks

The generated dependencies.lock file ensures that everyone on your team uses the same versions of dependencies, making builds deterministic and reducing the likelihood of bugs.

2. Using the Gradle Dependency Lock Plugin

For teams that need more advanced features, the gradle-versions-plugin can help track outdated dependencies. Run the following command to check for updates:

./gradlew dependencyUpdates

Then, to generate a lock file, run:

./gradlew dependencyUpdates

This command will generate a report listing all outdated dependencies in your project. For large projects, this helps ensure your dependencies stay current without manually checking each one.

3. Locking Specific Versions with strictly

If you need fine-grained control over which versions of a dependency are used (even with dynamic versioning), you can use Gradle’s strictly feature:

dependencies {
    implementation('com.vonage.client:client-sdk:2.7.+') {
        strictly '2.7.1'
    }
}

This will allow dynamic versioning but lock the version to 2.7.1, preventing any newer versions from being used unexpectedly.

4. IDE Warnings

Modern IDEs like IntelliJ IDEA or Android Studio will warn you when you use dynamic dependencies, making it easier to catch potential issues before they arise. They also often provide easy actions to update or lock dependency versions.

Best Practices for Dependency Management

  1. Explicitly define versions in production environments: Always specify the exact version in production code to avoid unexpected issues from new library versions.

  2. Use dependency locking: Lock dependencies at specific versions to ensure your builds are reproducible across all environments.

  3. Automate updates with tools: Consider using tools like Dependabot for automated dependency management in your repository. This helps you keep track of updates without manually checking each dependency.

  4. Review changelogs: Before updating dependencies, review their changelogs to ensure you’re aware of any breaking changes or new features.

  5. Test extensively: When using dynamic dependencies, make sure you have good test coverage in place to catch issues that may arise from changes in your dependencies.

Conclusion

Dynamic dependencies in Gradle offer convenience but can introduce risks, such as inconsistent builds and unpredictable bugs. It’s best to lock dependencies using Gradle’s built-in locking features or plugins like the gradle-versions-plugin. For production applications, it’s recommended to specify exact versions to avoid unexpected issues and ensure reproducibility. By adopting the right strategies and tools, you can confidently manage your dependencies and keep your builds stable.

Got any questions or comments? Join our thriving Developer Community on Slack, follow us on X (formerly Twitter), or subscribe to our Developer Newsletter. Stay connected, share your progress, and keep up with the latest developer news, tips, and events!

Share:

https://a.storyblok.com/f/270183/384x384/8ae5af43bb/igor-wojda.png
Igor WojdaVonage Alumni