/ android

Android - Understanding and dominating gradle dependencies

My latest post discussed gradle's dependencyInsight task. let's dive deeper into dependencies, direct and transitive, and gradle's dependency tools.

Direct VS. Transitive

Transitive dependency is an implied dependency, allowing your project to depend on libraries that depend on other libraries. The result is a dependency tree. These trees tend to get complex as your project requires more and more libraries to compile.
A direct, or "first level" dependency is one that you the developer explicitly import.

Exploring the dependency tree

We'll get back to transitive dependencies in a second. For now let's look at a project's entire dependency tree.
Invoking gradle dependencies --configuration compile results in something like this:

Already we can learn that this project has a lot of dependency conflicts. Those are marked by
{library name}:{required version} -> {actual version}

Conflicts

It is very common for for different modules and libraries to share the same dependencies. Who doesn't use apache-commons and GSon? Huh folks? Am I right? This guy knows what I'm talking about.

A problem arises when the dependency is the same, but each module or library expects a different version. This causes the project to contain multiple versions of the same class, and in turn causes the Dex tool to fail with the following error:

com.android.build.api.transform.TransformException: java.util.zip.ZipException: duplicate entry:

Resolving conflicts

If we didn't have gradle's transient dependency management, we would have to manually keep track of each dependency and resolve version conflicts by hand. This becomes harder the larger your project is. Lucky us we have gradle to handle the headache in one of several ways:

  1. The default behaviour is to settle on the newest version of the requested dependency. Please mind the word newest as opposed to highest version requested. This is fine as long as the versions are backward compatible with the lowest version your project expects.
  2. Second most popular resolution is to fail the build. You, the developer, would have to resolve the conflicts yourself.
  3. Configuring a first level dependency as forced is useful in cases where the conflicted transient dependency is already a direct (=first level) dependency.
  4. You may also configure a transient dependency as forced, thus forcing your entire project to settle on a specific version.
  5. If your build contains custom forks of the conflicted dependency, you may force gradle to use your own modules.
  6. You may tell gradle to not resolve transient dependencies of a specific library at all. Of course you would need to rely instead on manually added direct dependencies and/or existing transient ones.
  7. You may as well exclude specific transient dependencies referenced by a direct one.

Configuring build.gradle

The aforementioned strategies do not always translate directly to configuration switches.
Let's go over the basic techniques in achieving what we want:

Controlling dependencies project wide

This is achieved via the resolutionStrategy artifact.

Failing on version conflict

Add the following to your main project's build.gradle file.

configurations.all {
  resolutionStrategy {
    failOnVersionConflict()
  }
}
Forcing a specific dependency

The following example forces asm-all 3.3.1, commomns-io 1.4, and latest bolts where the major version number is 1 (i.e. bolts 1.x):

configurations.all {
  resolutionStrategy {
    force 'asm:asm-all:3.3.1', 'commons-io:commons-io:1.4', 'com.parse.bolts:bolts-android:1.+'
  }
}
Preferring own modules

Always prefer own modules:

configurations.all {
  resolutionStrategy {
    preferProjectModules()
  }
}

Replace all instances of commons-io with a custom module my-commons-io:

configurations.all {
  resolutionStrategy {
    dependencySubstitution {
      substitute module('commons-io:commons-io:2.4') with project(':my-commons-io')
    }
  }
}
Controlling specific dependencies

You may also fine grain your settings to a specific dependency on a specific project.

Excluding transient dependencies

Adding this to your dependencies tag would add the library appcompat-v7 but none of its own transient dependencies. You will have to make sure that all of the libraries needed dependencies are added either by adding direct dependencies yourself or by relying on transient dependencies referenced by other modules:

dependencies {
    compile('com.android.support:appcompat-v7:23.1.0') {
        transitive = false
    }
}

The default for transitive is true.

You can also exclude specific transient dependencies. The following example excludes the bolts library from being added, if needed by appcompat:

dependencies {
    compile('com.android.support:appcompat-v7:23.1.0') {
        exclude group: 'com.parse.bolts'
    }
}
Forcing a specific version

This forces your project to settle on bolts version 1.1:

dependencies {
    compile('com.parse.bolts:bolts-android:1.+') {
        force = true
    }
}

TL;DR

Controlling dependencies project wide

Example taken from gradle's official docs:

configurations.all {
  resolutionStrategy {
    // fail eagerly on version conflict (includes transitive dependencies)
    // e.g. multiple different versions of the same dependency (group and name are equal)
    failOnVersionConflict()

    // prefer modules that are part of this build (multi-project or composite build) over external modules
    preferProjectModules()

    // force certain versions of dependencies (including transitive)
    //  *append new forced modules:
    force 'asm:asm-all:3.3.1', 'commons-io:commons-io:1.4'
    //  *replace existing forced modules with new ones:
    forcedModules = ['asm:asm-all:3.3.1']

    // add dependency substitution rules
    dependencySubstitution {
      substitute module('org.gradle:api') with project(':api')
      substitute project(':util') with module('org.gradle:util:3.0')
    }

    // cache dynamic versions for 10 minutes
    cacheDynamicVersionsFor 10*60, 'seconds'
    // don't cache changing modules at all
    cacheChangingModulesFor 0, 'seconds'
  }
}
Controlling specific dependencies
dependencies {

    // We load commons-io but none of its transient dependencies
    compile('commons-io:commons-io:2.4') {
        transitive = false
    }

    // We exclude bolts altogether from appcompat's transient dependencies
    compile('com.android.support:appcompat-v7:23.1.0') {
        exclude group: 'com.parse.bolts'
    }

    // Force a specific version
    compile('com.parse.bolts:bolts-android:1.+') {
        force = true
    }
}
Exploring dependencies

To review all of your project's dependencies:
gradle dependencies --configuration CONFIGURATION_NAME

To inspect a specific dependency (more on this in this post):
gradle -q dependencyInsight --configuration CONFIGURATION_NAME --dependency DEPENDENCY_NAME