Table of contents:
- Gradle dependency resolution and conflict types
- Version Conflict
- Accidental dependency upgrades, handling multiple versions
- List all Gradle dependencies
- Resolving accidental upgrades — ResolutionStrategy
- Bonus: StrictVersionMatcher Gradle plugin
Gradle dependency resolution and conflict types
According to the Gradle documentation dependency resolution is a process that consists of two phases, which are repeated until the dependency graph is complete:
- When a new dependency is added to the graph, perform conflict resolution to determine which version should be added to the graph.
- When a specific dependency, that is a module with a version, is identified as part of the graph, retrieve its metadata so that its dependencies can be added in turn.
During those phases, Gradle may encounter conflicts and can resolve them automatically. While doing the dependency resolution Gradle can handle two types of conflicts:
- Version conflict — when two or more dependencies require a given dependency but with different versions.
- Implementation conflict — when the dependency graph contains a module that provides the same implementation.
For this blog, we are going to explore version conflict resolution.
Version Conflict
Can happen when two components depend on the same module but on different versions.
For instance: Our project depends on Firebase Analytics — “com.google.firebase:firebase-analytics:17.5.0”. It also depends on some other library which itself depends on Firebase Analytics but 18.0.3 version.
Gradle resolves this by selecting the highest version. In that case, 18.0.3 will be chosen. But that’s not the end. Gradle supports a concept of rich version declaration and there are several scenarios of how the version can be selected from the range. Highly recommended to read the whole document:
Problem — Accidental dependency upgrades, handling multiple versions
Let’s take the example given above when your project depends on the library which uses a higher version of dependency which you already have.
// Code snippet from Gradle docs dependencies { implementation 'org.apache.commons:commons-lang3:3.0' // the following dependency brings lang3 3.8.1 transitively implementation 'com.opencsv:opencsv:4.6' }
At the first glance, this code looks harmless but in reality, it can introduce lots of headaches. I can tell you from my experience, I had an issue when I was supporting the app with the minSdk 16. I was using Retrofit library version <2.7.0. I added another library which was by itself using Retrofit version 2.7.0. In this case, as mentioned already Gradle takes the highest version, but the problem was raised because retrofit 2.7.0 supports minSdk 21 instead of 16, so my app was crashing on older devices 😔.
List all Gradle dependencies
./gradlew -q app:dependencies --configuration {yourConfiguration}CompileClasspath
This command will output all the dependencies for the release configuration as a tree. Can be found in Gradle window in IDE as well (Gradle -> {module} -> Tasks -> help -> dependencies
Here is the part of the output example:
+--- androidx.databinding:viewbinding:4.1.2 | |
| \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 | |
+--- org.jetbrains.kotlin:kotlin-stdlib:1.4.32 | |
| +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32 | |
| \--- org.jetbrains:annotations:13.0 | |
+--- com.squareup.leakcanary:leakcanary-android:2.7 | |
| +--- com.squareup.leakcanary:leakcanary-android-core:2.7 | |
| | +--- com.squareup.leakcanary:shark-android:2.7 | |
| | | \--- com.squareup.leakcanary:shark:2.7 | |
| | | \--- com.squareup.leakcanary:shark-graph:2.7 | |
| | | \--- com.squareup.leakcanary:shark-hprof:2.7 | |
| | | \--- com.squareup.leakcanary:shark-log:2.7 | |
| | +--- com.squareup.leakcanary:leakcanary-object-watcher-android:2.7 | |
| | | +--- com.squareup.leakcanary:leakcanary-object-watcher:2.7 | |
| | | | \--- com.squareup.leakcanary:shark-log:2.7 | |
| | | +--- com.squareup.leakcanary:leakcanary-android-utils:2.7 | |
| | | | +--- com.squareup.leakcanary:shark-log:2.7 | |
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.32 (*) | |
| | | +--- com.squareup.curtains:curtains:1.0.1 | |
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.4.21 -> 1.4.32 (*) | |
| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.32 (*) | |
| | +--- com.squareup.leakcanary:leakcanary-object-watcher-android-androidx:2.7 | |
| | | +--- com.squareup.leakcanary:leakcanary-object-watcher-android:2.7 (*) | |
| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.32 (*) | |
| | +--- com.squareup.leakcanary:leakcanary-object-watcher-android-support-fragments:2.7 | |
| | | +--- com.squareup.leakcanary:leakcanary-object-watcher-android:2.7 (*) | |
| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.32 (*) | |
| | +--- com.squareup.leakcanary:plumber-android:2.7 | |
| | | +--- com.squareup.leakcanary:shark-log:2.7 | |
| | | +--- com.squareup.leakcanary:leakcanary-android-utils:2.7 (*) | |
| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.32 (*) | |
| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.32 (*) | |
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.32 (*) | |
+--- com.squareup.leakcanary:plumber-android:2.7 (*) |
Gradle dependencies tree view
Job Offers
Legend:
+ ---
— start of a dependency branch
|
branch with the libraries it depends on
\---
end of a dependency branch
(c)
— dependency constraint
(*)
— dependencies omitted (listed previously)
Look closely at the output, if you can spot ->
symbol? This is the point where Gradle does version conflict resolution and chooses the highest number. In this example, we can clearly see several dependencies being swapped.
Resolving accidental upgrades — ResolutionStrategy
Gradle provides API for the cases described above. I will write down the most crucial ones. It defines strategies about dependency resolution, sometimes forcing to fail or replace conflicting dependencies.
configurations.all { resolutionStrategy { failOnVersionConflict() } }
failOnVersionConflict — will fail eagerly on version conflict (includes transitive dependencies)
failOnDynamicVersions– will prevent the use of dynamic versions
preferProjectModules — prefer modules that are part of this build (multi-project or composite build) over external modules
force ‘lib1:12.0.0’, ‘lib1:13.0.0’ — force certain versions of dependencies (including transitive)
dependencySubstitution — this is a general rule of substitution, an example from Gradle docs:
dependencySubstitution { substitute module('org.gradle:api') using project(':api') substitute project(':utl') using module('org.gradle:util:3.0') }
ResolutionStrategy also provides caching for dynamic versions and etc, please refer to docs for detailed info.
Example: We can utilize ResolutionStrategy API and be forced to downgrade a dependency in that manner, and I believe this is one of the most used in projects. This rule swaps the latest version of Firebase Analytics in that case 18.0.3 with the older one 17.5.0
configurations.all { resolutionStrategy { force 'com.google.firebase:firebase-analytics:18.0.3', 'com.google.firebase:firebase-analytics:17.5.0' } } output: +--- com.google.firebase:firebase-core:18.0.3 | \--- com.google.firebase:firebase-analytics:18.0.3 -> 17.5.0 (*)
Bonus: StrictVersionMatcher Gradle plugin
Google Play services and Firebase libraries are maintained individually and developed quickly. To keep that pace and do not break things for developers the Google Services Gradle plugin checks for compatible versions of Google Play services and Firebase libraries.
A version of one library might be incompatible with a specific version of another library. To help handle this situation, several Gradle plugins provide guidance regarding these version mismatches. The logic in these plugins is similar to the logic in a failOnVersionConflict()
rule for a ResolutionStrategy that’s associated with Google Play services and Firebase dependencies.
However, if you are not using the Google Play Services plugin but still want to protect your builds, you can use the standalone version matcher plugin and does the exact same thing.
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
Plugin’s code is on Github: https://github.com/google/play-services-plugins.
Follow me on Twitter