Gradle: managing dependencies

  • Tutorial
Dependency management is one of the most important features in the arsenal of build systems. With the advent of Gradle as the main build system for Android projects, there has been a significant shift in the part of dependency management, the era of manually copying JAR files and long dances with a tambourine around failed project configurations has ended. The article discusses the basics of dependency management in Gradle, provides in-depth practical examples, small life hacks and links to the right places in the documentation.







Repository


Как известно, Gradle не имеет собственных репозиториев и в качестве источника зависимостей использует Maven- и Ivy-репозитории. При этом интерфейс для работы с репозиториями не отличается на базовом уровне, более развёрнуто об отличиях параметров вы можете узнать по ссылкам IvyArtifactRepository и MavenArtifactRepository. Стоит отметить, что в качестве url могут использоваться ‘http’, ‘https’ или ‘file’ протоколы. Порядок, в котором записаны репозитории, влияет на порядок поиска зависимости в репозиториях.

// build.gradle
repositories {
	maven {
		url "http://example.com"
	}
	ivy {
		url "http://example.com"
	}
}


Объявление зависимостей


// build.gradle
apply plugin: 'java'
repositories {
	mavenCentral()
}
dependencies {
	compile group: 'com.googlecode.jsontoken', name: 'jsontoken', version: '1.1'
	testCompile group: 'junit', name: 'junit', version: '4.+'
}


In the above example, you see a build script in which two dependencies for different configurations (compile and testCompile) of compiling a project are connected. JsonToken will be connected during compilation of the project and compilation of project tests, jUnit only during compilation of project tests. More information about compilation configurations is available here .

You can also see that we connect the jUnit dependency as dynamic (+), i.e. the latest available version 4. + will be used, and we won’t need to keep track of minor updates (I recommend not using this feature in the compile type of compiling the application, because unexpected, possibly difficult to localize problems may appear).

For an example with a jUnit dependency, consider the standard Gradle mechanism for finding the necessary dependency:

1. Зависимость
	compile ("org.junit:junit:4.+")
2. Получение версии модуля
	group:	"org.junit"
	name:		"junit"
	version:	"4.+"
3. Получение списка возможных версий модуля
	[junit:4.1]
	…
	[junit:4.12]
4. Выбор одной версии зависимости
	[junit:4.12]
5. Получение версии зависимости
	[junit:4.12]
		dependencies { … }
		artifacts { … }
6. Присоединение артефактов зависимости к проекту
	junit-4.12.jar
	junit-4.12-source.jar
	junit-4.12-javadoc.zip


Cache


Gradle has a caching system that, by default, stores dependencies for 24 hours, but this behavior can be overridden.

// build.gradle
configurations.all {
	resolutionStrategy.cacheChangingModulesFor 4, 'hours'
	resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes'
}


After the time set for storing data in the cache has run out, the system, when starting tasks, will first check the ability to update dynamic and changing dependencies and update them if necessary.

Gradle tries not to upload files that were previously downloaded, and uses a verification system for this, even if the URL / file sources are different. Gradle always checks the cache (URL, version and module name, cache of other versions of Gradle, Maven cache), HTTP request headers (Date, Content-Length, ETag) and SHA1 hash, if available. If no matches are found, the system will download the file.

There are also two parameters in the build system, using which at startup you can change the caching policy for a specific task.

- –offline - Gradle will never try to contact the network to check for dependency updates.
- –refresh-dependencies - Gradle will try to update all dependencies. It is convenient to use when data in the cache is corrupted. Verifies cached data and, if different, updates them.

You can read more about dependency caching in the Gradle User Guide .

Types of Dependencies


There are several kinds of dependencies in Gradle. The most commonly used are:

- External project dependencies - dependencies downloaded from external repositories;
// build.gradle
dependencies {
	compile "com.android.support:appcompat-v7:23.1.1"
}


- Project dependencies - dependence on the module (subproject) within the framework of one project;
// build.gradle
dependencies {
	compile project(':subproject')
}


- File dependencies - dependencies connected as files (jar / aar archives).
build.gradle
repositories {
	flatDir {
		dirs 'aarlibs' // инициализируем папку, хранящую aar-архивы как репозиторий
	}
}
dependencies {
	compile(name:'android_library', ext:'aar') // подключаем aar-зависимость
	compile files('libs/a.jar', 'libs/b.jar')
	compile fileTree(dir: 'libs', include: '*.jar')
}


There are also client module dependencies, Gradle API dependencies, and local Groovy dependencies. They are rarely used, therefore, in the framework of this article we will not disassemble them, but you can read the documentation about them here .

Dependency tree


Each external or design dependency may contain its own dependencies, which must be taken into account and downloaded. Thus, when the compilation is performed, dependencies are loaded for the selected configuration and a dependency tree is built, the human representation of which can be seen by executing the Gradle task 'dependencies' in Android Studio or the gradle% module_name%: dependencies command in the console, located in the project root folder. In response, you will get a list of dependency trees for each of the available configurations.

Using the configuration parameter, specify the configuration name to see the dependency tree of only the specified configuration.

Let's take specially prepared sources of the repository located on githuband try to get the dependency tree for a specific configuration (at the moment the project is in state 0, i.e. build.gradle is used as build.gradle.0):



After analyzing the dependency tree, you can see that the app module uses two dependencies external dependencies (appcompat and guava), as well as two design dependencies (first and second), which in turn use the jsontoken library versions 1.0 and 1.1 as an external dependency. Obviously, a project cannot contain two versions of the same library in Classpath, and there is no need for this. At this point, Gradle includes a conflict resolution module.

Conflict resolution


Gradle DSL contains a component used to resolve dependency conflicts . If you look at the jsontoken library dependencies in the above dependency tree, we will see them only once. For the second module, dependencies of the jsontoken library are not specified, and the output of the dependency itself additionally contains '-> 1.1', which means that the library version 1.0 is not used, but was automatically replaced with version 1.1 using the Gradle conflict resolution module.

To explain how the conflict was resolved, you can also use the Gradle task dependencyInsight, for example:



It is worth noting that version 1.1 is selected as a result of conflict resolution, it is also possible to select as a result of other rules (for example: selected by force or selected by rule). The article will give examples of the use of rules that affect the strategy for resolving dependencies, and after completing the dependencyInsight task, you can see the reason for choosing a specific version of the library at each of the steps below. To do this, when moving to each stage, you can independently execute the dependencyInsight task.

If necessary, it is possible to override the logic of the Gradle conflict resolution module, for example, by instructing Gradle to fall when conflicts are detected during project configuration. (state 1)

// build.gradle
// …
configurations.compile.resolutionStrategy {
	failOnVersionConflict()
}


After which, even when you try to build the Gradle dependency tree, tasks will be interrupted due to a conflict in the application dependencies.



The problem has four solutions:

The first option is to delete lines that override the conflict resolution strategy.

The second option is to add the rule of mandatory use of the jsonToken library to the conflict resolution strategy, indicating the specific version (state 2):

// build.gradle
// …
configurations.compile.resolutionStrategy {
	force 'com.googlecode.jsontoken:jsontoken:1.1'
	failOnVersionConflict()
}


When applying this solution, the dependency tree will look like this:



The third option is to add the jsonToken library explicitly as a dependency for the app project and assign the force parameter to the dependency, which will explicitly indicate which version of the library to use. (state 3)

// build.gradle
// …
dependencies {
	compile fileTree(dir: 'libs', include: ['*.jar'])
	compile 'com.android.support:appcompat-v7:23.1.1'
	compile 'com.google.guava:guava:+'
	compile project(":first")
	compile project(":second")
	compile ('com.googlecode.jsontoken:jsontoken:1.1') {
		force = true
	}
}


And the dependency tree will look like this: The



fourth option is to exclude jsontoken from one of the project dependencies from its own dependencies using the exclude parameter. (state 4)

// build.gradle
dependencies {
	compile fileTree(dir: 'libs', include: ['*.jar'])
	compile 'com.android.support:appcompat-v7:23.1.1'
	compile 'com.google.guava:guava:+'
	compile project(":first")
	compile(project(":second")) {
		exclude group: "com.googlecode.jsontoken", module: 'jsontoken'
	}
}


And the dependency tree will look like this:



It is worth noting that exclude does not have to pass both parameters at the same time, only one can be used.

But despite the correct output of the dependency tree, when you try to build the application, Gradle will return an error:



The cause of the error can be understood from the output of the build task execution messages - the GwtCompatible class with the same package name is contained in several dependencies. And this is true, the fact is that the app project uses the guava library as a dependency, and the jsontoken library uses the outdated Google Collections in the dependencies. Google Collections is part of Guava, and sharing them in the same project is not possible.

There are three options for achieving a successful project build:

The first is to remove guava from the app module dependencies. If you use only the part of Guava that is contained in Google Collections, then the proposed solution will be good.

The second is to exclude Google Collections from the first module. We can achieve this using the exception described above or configuration rules. Consider both options, using exceptions first (state 5)

// build.gradle
dependencies {
	compile fileTree(dir: 'libs', include: ['*.jar'])
	compile 'com.android.support:appcompat-v7:23.1.1'
	compile 'com.google.guava:guava:+'
	compile(project(":first")) {
		exclude module: 'google-collections'
	}
	compile(project(":second")) {
		exclude group: "com.googlecode.jsontoken", module: 'jsontoken'
	}
}


An example of using configuration rules (state 6):

//build.gradle
configurations.all {
	exclude group: 'com.google.collections', module: 'google-collections'
}
dependencies {
	compile fileTree(dir: 'libs', include: ['*.jar'])
	compile 'com.android.support:appcompat-v7:23.1.1'
	compile 'com.google.guava:guava:+'
	compile project(":first")
	compile(project(":second")) {
		exclude group: "com.googlecode.jsontoken", module: 'jsontoken'
	}
}


The dependency tree for both implementations of the Google Collections exception will be identical.



The third option is to use the module substitution functionality (state 7):

// build.gradle
dependencies {
	modules {
		module('com.google.collections:google-collections') {
			replacedBy('com.google.guava:guava')
		}
	}
	compile fileTree(dir: 'libs', include: ['*.jar'])
	compile 'com.android.support:appcompat-v7:23.1.1'
	compile 'com.google.guava:guava:+'
	compile project(":first")
	compile(project(":second")) {
		exclude group: "com.googlecode.jsontoken", module: 'jsontoken'
	}
}


The dependency tree will look like this:



You need to take into account that if you leave the predefined conflict resolution logic, which indicates to interrupt the assembly in the presence of any conflict, then the execution of any task will be interrupted at the configuration stage. In other words, the use of module replacement rules is one of the rules of a conflict resolution strategy between dependencies.

It is also important to note that the last of the options mentioned is the most flexible, because when you remove guava from the list of Gradle dependencies, Google Collections will be saved in the project, and the functionality that depends on it will be able to continue execution. And the dependency tree will look like this:



After each of the options, we will achieve success in the form of a compiled and launched application.

But let's look at a different situation (state 8), we have one single greatly reduced (to reduce the size of the screenshots) dynamic dependence of wiremock. We use it purely for training purposes, imagine instead a library that your colleague supplies, he can release a new version at any time, and you certainly need to use the latest version:

// build.gradle
configurations.all {
	exclude group: 'org.apache.httpcomponents', module: 'httpclient'
	exclude group: 'org.json', module: 'json'
	exclude group: 'org.eclipse.jetty'
	exclude group: 'com.fasterxml.jackson.core'
	exclude group: 'com.jayway.jsonpath'
}
dependencies {
	compile 'com.github.tomakehurst:wiremock:+'
}


The dependency tree is as follows:



As you can see, Gradle downloads the latest available version of wiremock, which is beta. The situation is normal for debug assemblies, but if we are going to provide the assembly to users, then we definitely need to use the release version to be sure of the quality of the application. But at the same time, due to the constant need to use the latest version and frequent releases, there is no way to refuse to dynamically indicate the version of wiremock. The solution to this problem is to write your own rules for the strategy of selecting dependency versions:

// build.gradle
//…
configurations.all {
	//…
	resolutionStrategy {
		componentSelection {
			all { selection ->
				if (selection.candidate.version.contains('alpha')
					|| selection.candidate.version.contains('beta')) {
						selection.reject("rejecting non-final")
				}
			}
		}
	}
}


It’s worth noting that this rule will apply to all dependencies, and not just to wiremock.
After that, starting the task of displaying the dependency tree in information mode, we will see how beta versions of the library are discarded, and the reason why they were discarded. In the end, a stable version 1.58 will be selected:



But during testing it was discovered that a critical bug was present in version 1.58, and the assembly could not be released in this state. You can solve this problem by writing another rule for choosing the version of the dependency:

// build.gradle
//…
configurations.all {
	//…
	resolutionStrategy {
		componentSelection {
			// …
			withModule('com.github.tomakehurst:wiremock') { selection ->
				if (selection.candidate.version == "1.58") {
					selection.reject("known bad version")
				}
			}
		}
	}
}


After that, wiremock 1.58 will also be discarded, and version 1.57 will start to be used, and the dependency tree will look like this:



Conclusion


Despite the fact that the article turned out to be quite voluminous, the topic of Dependency Management in Gradle contains a lot of information that was not announced within the framework of this article. It’s best to dive deeper into this world with the help of the official User Guide coupled with the Gradle DSL documentation , which will take a lot of time to study.

But as a result, you will get the opportunity to save dozens of hours, both thanks to automation, and thanks to an understanding of what needs to be done when various bugs are displayed. For example, recently bugs with 65K methods and Multidex have been quite active, but thanks to the competent viewing of dependencies and using exclude, problems are solved very quickly.

Read also:Gradle: 5 developer benefits

Only registered users can participate in the survey. Please come in.

What version of Gradle are you using in production?

  • 11.1% <Gradle 2.4 17
  • 10.4% Gradle 2.4 16
  • 9.1% Gradle 2.4 <=> Gradle 2.8 14
  • 9.1% Gradle 2.8 14
  • 12.4% Gradle 2.9 19
  • 47.7% Gradle 2.10 73

Also popular now: