Article
Développement mobile
17.3.2016
Perturbation continuum temporel
6 minutes
Temps d'un café
La gestion des dépendances avec Gradle
Vincent Brison
Note : Ce contenu a été créé avant que Fabernovel ne fasse partie du groupe EY, le 5 juillet 2022.

La gestion des dépendances par Gradle est incontestablement l’un des énormes avantages de ce système de build, en particulier pour Android, où il permet de gérer finement les nombreuses dépendances (les Play Services, les librairies de support, ou tout autres librairies) via le système de dépendances de Maven.

Cette gestion automatique des dépendances par Gradle soulève néanmoins des questions :

  • Que se passe-t-il vraiment si deux dépendances au sein d’un projet ont une dépendance vers une troisième dépendance mais dans une version différente ?
  • Mais à quoi sert exactement le mot clé “transitive” ?
  • Peut on avoir des dépendances optionnelles ?

Cet article tente de répondre à ces questions de manière claire et explicite aux travers d’exemples communs au monde Android.

Projet de démonstration

Un projet servant d’exemple est disponible sur GitHub.

Vous êtes donc fortement invités à le cloner. Chaque exemple correspond à un commit indiqué. Le projet comporte un module gradle app.

Ajouter une dépendance

Exemple : (<a href="https://github.com/applidium/gradle-dependencies-demo/tree/2a7e6ea45baa879bd50a172e2a16d5a8cf487662">commit</a>)

Pour utiliser la librairie de font calligraphy, on ajoute la dépendance ainsi au build.gradle de notre module app :

compile 'uk.co.chrisjenx:calligraphy:2.1.0'

C’est l’exemple le plus classique d’utilisation de compile, et dans la gestion de dépendance par gradle de manière générale.

Que se passe t’il si cette dépendance a elle même des dépendances ? Pour lister les dépendances d’un module gradle nommé app, on peut utiliser la commande suivante :

./gradlew dependencies app:dependencies

Cette commande listera les dépendances pour chacune des configurations (voir la documentation) du module app. Il peut être intéressant de filtrer les configurations afin de limiter l’output à la configuration compile qui nous intéresse dans le cas présent.

./gradlew dependencies app:dependencies --configuration compile

On obtient en résultat :

compile - Classpath for compiling the main sources. --- uk.co.chrisjenx:calligraphy:2.1.0 \--- com.android.support:appcompat-v7:22.1.1 \--- com.android.support:support-v4:22.1.1 \--- com.android.support:support-annotations:22.1.1

On peut voir que calligraphy a pour dépendance appcompat-v7, qui elle même dépend de support-v4, qui dépend de support-annotations. Par défaut, ces dépendances seront satisfaites de manière transparente, pas besoin de les ajouter à la liste des dépendances du module app. Ces dépendances sont résolues de manière dite transitive.

Principe de résolution transitive

Exemple : (<a href="https://github.com/applidium/gradle-dependencies-demo/tree/ce64984d591f0e252f9d0f618308ef3702487255">commit</a>)

Imaginons que notre module app utilisait déjà la librairie de support appcompat-v7, mais dans une version supérieure, à savoir 23.1.1. Quelle version de celle ci est finalement utilisée, sachant que calligraphy a besoin de la 22.1.1 ? Affichons l’analyse des dépendances par gradle :

compile – Classpath for compiling the main sources.

+--- uk.co.chrisjenx:calligraphy:2.1.0 | --- com.android.support:appcompat-v7:22.1.1 -> 23.1.1 | --- com.android.support:support-v4:23.1.1 | --- com.android.support:support-annotations:23.1.1 --- com.android.support:appcompat-v7:23.1.1 (*)

On voit que la dépendance de calligraphy vers appcompat-v7:22.1.1 est transformée en dépendance vers appcompat-v7:23.1.1.

Par défaut, les dépendances sont transitives, ce qui fait que par récursivité, toutes les dépendances sont satisfaites. C’est ainsi que support-v4 et support-annotations sont automatiquement importées et traitées comme dépendance du projet (il n’y a pas besoin de les ajouter au build.gradle).

Stratégie de gestion des dépendances

Exemple : (<a href="https://github.com/applidium/gradle-dependencies-demo/tree/ab6745197cb1abcf55eab428a6fc9784d89eb7e7">commit</a>)

Pour être informé que dans les dépendances du projet, deux versions différentes sont demandées, on peut ajouter le bloc suivant au build.gradle :

configurations.all { resolutionStrategy { failOnVersionConflict() } }

Ce bloc provoquera une erreur lors de la compilation en cas de conflit de version, vous permettant de gérer manuellement le conflit de version. Gradle applique par défaut ses propres stratégies de résolution de dépendance, où, en cas de de conflit, la version la plus récente est utilisée. Il est possible de définir ses propres stratégies de résolution de dépendance, dont le bloc ci dessus en est une (documentation).

Filtrer les dépendances

Exemple : (<a href="https://github.com/applidium/gradle-dependencies-demo/tree/f1d09146baabe6ec440ac40e9cdfdc507acf4187">commit</a>)

Par ailleurs, on peut également directement agir sur la configuration d’intégration des dépendances. Prenons comme exemple la librairie simple-xml.

Le résultat de son ajout aux dépendances (commits D) est le suivant :

compile - Classpath for compiling the main sources. --- org.simpleframework:simple-xml:2.7.1 +--- stax:stax-api:1.0.1 +--- stax:stax:1.2.0 | \--- stax:stax-api:1.0.1 \--- xpp3:xpp3:1.1.3.3

Or cette configuration est incompatible avec Android pour les raisons suivantes :

  • xpp3 fait partie intégrante du framework Android. Les classes y sont déjà présentes. On ne peut donc inclure d’autres classes (même si ce sont les mêmes que celles du framework) qui auraient le même package et le même nom.
  • stax utilise un nom de package protégé par le framework Android (java.xml.*).Néanmoins simple-xml fonctionne très bien avec Android car sa dépendance à stax est optionnelle (vérifiée au runtime par la présence des classes correspondantes).

Mais intégré de la sorte, la compilation est impossible à cause de stax (le système de build ignore automatiquement xpp3). Il faut donc exclure stax (puisque non nécessaire, et inutilisable sur Android). On peut gérer ainsi finalement l’intégration de la dépendance (commit) :

compile('org.simpleframework:simple-xml:2.7.1'){ exclude module: 'stax' exclude module: 'stax-api' exclude module: 'xpp3' // optional }

La DSL associée est documentée ici.

Côté librairie : Etre flexible avec Optional

On a vu dans l’exemple précédent que simple-xml pouvait se passer de stax pour fonctionner. Néanmoins, si on regarde son pom.xml, on voit que stax est une dépendance non optionnelle.

Or il est possible de définir une dépendance comme étant optionnelle (au sens Maven du terme). C’est à dire que si une librairie L(ibrary) définit la librairie O(ptional) comme étant optionnelle, la résolution transitive des dépendances de L ne provoquera pas l’inclusion de la dépendance O.

Dans le cas de simple-xml, si stax et xpp3 étaient des dépendances optionnelles, cela permettrait de déclarer la dépendance à simple-xml ainsi :

compile 'org.simpleframework:simple-xml:2.7.1'

C’est le cas de Retrofit 1 où la dépendance OkHttp est optional. Ainsi au runtime, si OkHttp est présent, Retrofit utilisera OkHttp comme client Http par defaut, sinon il utilisera le client Http par defaut de la plateforme actuelle. Néanmoins, on peut observer que Retrofit n’utilise pas de base Gradle, mais Maven. On pourrait expliquer ce choix pour la raison suivante ; La notion optional n’est pas supportée nativement par Gradle, néanmoins des solutions existent pour disposer de cette possibilité :

  • Comme l’indique ce thread, il est possible de recréer cette notion “à la main”.
  • Le thread ci-dessus le suggère, il existe un plugin qui permet d’accèder à optional dans une syntaxe Gradle claire et consise (gradle-extra-configurations-plugin).

Conclusion

La gestion des dépendances par Gradle est un outil très puissant. Maitriser les bases de la configuration de ces dépendances permet de gérer correctement et intelligemment l’évolution des dépendances tout au long de la vie d’un projet. Par ailleurs, une utilisation précise de la définition des dépendances par les développeurs de librairies peut s’avérer judicieuse, pour segmenter correctement les besoins des libraries, comme le fait Retrofit.

Remerciements à Adrien Couque pour m’avoir grandement guidé dans l’élaboration de cet article.

No items found.
Pour aller plus loin :