Don't use dynamic versions for your dependencies

Everyone, please, to stop using dynamic versions for your dependencies.

In Gradle, dynamic versions use the + sign like so:

compile 'com.android.support:appcompat-v7:23.0.+'

Ideally, your builds should be predictable and consistent. Identical source code should yield the same result, every time you build.

Dynamic versions add nondeterminism to your build. In other words, the same source code may not yield the same output. This behavior manifests itself in all sorts of nasty problems:

  • Dependencies can unexpectedly introduce behavior changes to your app. Read your changelogs carefully!

  • The same source built on two different machines can differ. How many times have you said "but it works on my machine?"

  • Similarly, builds built on the same machine but at different times can differ. I've wasted so much time on builds that worked one minute then broke the next.

  • Past builds cannot be reproduced perfectly. This makes it difficult to revert safely.

  • There are security implications if a bad actor introduces a malicious version of a dependency.

The solution is simple: always specify your dependency versions explicitly.

But But But...!

Here's a couple of arguments I've heard for using dynamic versioning and why I disagree.

I don't want to bother updating dependencies.

Your laziness will likely cost you more time down the road fixing some problem created by dynamic versions.

Bugfix updates should be safe and absorbed as quickly as possible.

In other words, if you're using 23.0.+, it should never break because the patch version should only update for bug fixes.

There are two problems with this idea. First, you have to trust that the library is using versions to denote patches correctly. And second, you have to trust that the developer never writes a bug into a patch version. Two big ifs; not worth the risk.

My library needs to use dynamic versioning; otherwise I have to release a new version of the library every time one of its dependencies updates.

This is wrong on two counts.

First, users can update your library's dependencies without a new version of your library. They just need to define the dependency on their own:

// Depends on appcompat-v7:23.0.0
compile 'com.trello:rxlifecycle-components:0.3.0'

// Even though rxlifecycle-components uses 23.0.0,
// the build will ultimately use 23.0.1 as provided below
compile 'com.android.support:appcompat-v7:23.0.1'

Second, if a dependency updates in a way that breaks your library, then you need to manually update your library anyways!

In other words: it isn't needed and it won't save you effort.

Developers can't be trusted to update their dependencies.

Otherwise known as the Fabric argument for why you should use + with their plugin:

// The Fabric Gradle plugin uses an open ended version to
// react quickly to Android tooling updates
classpath 'io.fabric.tools:gradle:1.+'

The choice here is thus: you can either break your build after updating tools (which is an expected occasional outcome), or your build can randomly break when you unknowingly get a new version of Fabric.

I would rather take the route where I know what changed vs. the one where I have to hunt down the root problem.

Removing Dynamic Versions

Hopefully you're itching to get rid of all your dynamic versions. How do you do it?

For Android developers, there's a couple handy tools you can use. If you're using Android Studio then you can select a dynamic version and use ALT + ENTER to pop up the quickfix dialog. From there select "Replace with specific version."

Replace with specific version demonstration

If you want to see all your dependencies at once then use the task androidDependencies:

$ ./gradlew androidDependencies

If you're not building Android then you'll have to resort to the dependencies task:

$ ./gradlew dependencies

Often times you'll need to specify the subproject you're targeting for this task. Supposing my app is in app-dir/, this is how you'd do it:

$ ./gradlew :app-dir:dependencies

These tasks show most dependencies but they're missing the buildscript ones (i.e., plugins like Fabric). As far as I know, there's no easy way to list these. You have to add your own task that prints it out for you:

// Add this to your build.gradle
task buildscriptDependencies << {
    buildscript.configurations.classpath.each { println it }
}

Then you can run this new script to get the buildscript versions:

$ ./gradlew buildscriptDependencies

Once you've purged your dynamic versions, I suggest using the gradle-versions-plugin on a regular basis to update your dependencies.

Exception

As with everything in programming, there are always exceptions. There's a plugin from Netflix that partially solves dynamic version problems. It allows you to define a range for a dependency but then also locks that dependency in place afterwards. This doesn't save you from a dependency update randomly breaking your build but it does keep builds consistent.

Personally, updating dependencies by hand is simple enough and I like to read changelogs to make sure nothing is catching me off guard. I haven't tried this plugin as a result, but it could be appropriate if you have enough tests in place to protect you from dependencies breaking code.


Thanks to the many people who proofread this article: Wojtek Kaliciński, Johan Nilsson, Ryan Harter, Sebastiano Poggi, and Jake Wharton.