Blog Infos
Author
Published
Topics
,
Published

Table of contents:

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.

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:

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 😔.

./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

Job Offers


    Senior Android Engineer

    Carly Solutions GmbH
    Munich
    • Full Time
    apply now

    Senior Android Developer

    SumUp
    Berlin
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

, , ,

Ultimate Iteration Speeds with Gradle Configuration Cache

A dive into what is Gradle Configuration Cache and how it works, why you want to have it enabled, and how to debug unexpected configuration cache misses for local and CI workflows.
Watch Video

Ultimate Iteration Speeds with Gradle Configuration Cache

Aurimas Liutikas
Software Engineer
Google / Gradle Fellow

Ultimate Iteration Speeds with Gradle Configuration Cache

Aurimas Liutikas
Software Engineer
Google / Gradle Fell ...

Ultimate Iteration Speeds with Gradle Configuration Cache

Aurimas Liutikas
Software Engineer
Google / Gradle Fellow

Jobs

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.

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 (*)

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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Managing dependencies in a single module project is pretty simple, but when you start…
READ MORE
blog
With the rise of Kotlin Multiplatform (KMP), it gave birth to a cross-platform UI…
READ MORE
blog

Running Instrumented Tests in a Gradle task

During the latest Google I/O, a lot of great new technologies were shown. The…
READ MORE
blog
Many of us have faced Groovy difficulties and used to convert it to Kotlin…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu