Les plugins Gradle précompilés : dites adieu aux scripts répétitifs

Tu as déjà ouvert le fichier build.gradle d’un nouveau module et copié-collé le contenu d’un autre module en te disant « je réglerai ça plus tard » ?

Plus tard n’arrive jamais. Et pendant ce temps, la dette technique s’accumule.

Pendant des années, j’ai subi ce problème sans vraiment le résoudre. Sur des projets à dizaines de modules chez Coyote et Deezer — des vraies apps Android utilisées par des millions d’utilisateurs — la duplication des scripts Gradle était l’un de ces problèmes invisibles qui pourrit la vie de toute une équipe sans que personne ne prenne le temps de le résoudre vraiment.

Une modification dans les options de compilation ? Répercutée à la main dans 20 fichiers. Un nouveau module à créer ? Copier-coller, croiser les doigts, chercher les erreurs. Un vrai cauchemar.

Aujourd’hui, je t’explique comment en finir une bonne fois pour toutes avec les plugins Gradle précompilés — la solution simple et élégante que j’aurais voulu connaître bien plus tôt.

Comment fonctionnent les plugins Gradle précompilés ?

métaphore entre un plugin Gradle précompilé et un appartement clef en main

Un plugin Gradle précompilé, c’est comme un appartement à la location clé en main.

Tu n’as pas besoin de te soucier des meubles à acheter, de la cuisine à installer, de la peinture rose à repeindre en blanc. Tout est déjà fait. Tu récupères les clés et tu débales tes affaires.

Dans le cadre de ton application Android, le plugin Gradle précompilé te fournit la configuration de base pour chacun de tes modules. Tu ne l’écris plus à chaque fois — tu l’utilises tel quel.

Techniquement, les plugins Gradle précompilés sont des plugins écrits en Kotlin, compilés en bytecode avant l’exécution. Contrairement aux scripts Gradle classiques en Kotlin DSL qui sont interprétés à la volée, le plugin précompilé est déjà prêt quand Gradle en a besoin.

Les avantages sont concrets et immédiats :

  1. Builds plus rapides. Le plugin est déjà compilé en bytecode — Gradle n’a plus à l’interpréter à chaque build.
  2. Code organisé et maintenable. La logique de build est centralisée dans un seul endroit. Tes scripts build.gradle deviennent propres, courts, lisibles.
  3. Réutilisabilité totale. Un seul plugin, utilisé dans tous tes modules. Tu modifies un seul fichier — le changement se propage partout.

Le problème : la duplication des scripts build.gradle

Pour que ce soit concret, prenons l’exemple d’une application Android organisée en trois modules :

architecture d'un plugin Gradle précompilé
  • App — l’application Android principale
  • Authentication — le module de gestion de l’authentification
  • Database — le module de stockage des données

Voici le script build.gradle.kts du module Authentication :

//build.gradle.kts du module Authentication sans plugin précompilé

plugins {
    id("com.android.library")
}

android {
    namespace = "com.precompiledplugin.example.authentication"
    compileSdk = 34
    defaultConfig {
        minSdk = 30
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
}

dependencies {
    implementation(libs.androidCoreKtx)
    implementation(libs.bundles.kotlinBundle)
    // Dépendances d'une bibliothèque permettant de faire de l'authentification 
    // ....
}

Nous pouvons voir dans ce script Gradle que :

  • Le bloc plugins {………} : définit ce module en tant que librairie Android
  • Le bloc android {………} : définit les paramètres de compilation de l’application Android
  • Le bloc dependencies {………} : définit les dépendances dont a besoin le code du module pour être compilé 

Maintenant regarde le script du module Database :

//build.gradle.kts du module Database sans plugin précompilé

plugins {
    id("com.android.library")
}

android {
    namespace = "com.precompiledplugin.example.database"
    compileSdk = 34
    defaultConfig {
        minSdk = 30
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
}

dependencies {
    implementation(libs.androidCoreKtx)
    implementation(libs.bundles.kotlinBundle)
    // Dépendances d'une bibliothèque permettant de faire de la base de donnée 
    // ....
}

99% identiques. Seul le namespace et les dépendances métier diffèrent.

Maintenant imagine ça sur 20, 30, 50 modules. C’est le quotidien de la plupart des grandes apps Android en production. Et crois-moi — quand tu dois changer la compileSdk sur l’ensemble du projet, l’envie de tout plaquer est réelle.

Comment créer un plugin Gradle précompilés

Avant de créer ton premier plugin précompilé, il y a un concept clé à connaître : le dossier buildSrc 

Ce répertoire n’est pas créé par défaut dans ton projet Android. C’est pourtant l’endroit prévu par Gradle pour stocker ta logique de build et tes plugins — séparément du code de ton application. Une fois ce dossier présent à la racine de ton projet, Gradle le reconnaît automatiquement et compile tout ce qu’il contient avant le reste du build.

C’est là que va vivre ton plugin précompilé. Et voici comment le créer concrètement.

La solution : le plugin Gradle précompilé

Crée l’arborescence suivante dans ton projet :

Arborescence du dossier buildSrc dans un projet Android : src/main/kotlin/precompiled_plugin/android-library-base.gradle.kts — structure pour créer un plugin Gradle précompilé

Pour éviter cette duplication, nous allons créer un plugin Gradle précompilé qui sera ensuite utilisé dans les modules Authentication et Database. Les noms des dossiers et fichiers ci-dessous sont donnés à titre d’exemple mais pouvez les nommer comme bon vous semble 😉

Pour cela il faut :

  • Créer l’arborescence buildSrc/src/main/kotlin/precompiled_plugin/
  • Créer un script gradle dans le dossier précédent : android-library-base.gradle.kts et le remplir avec le code suivant :

Et voici le contenu de ton premier plugin précompilé :

//plugin précompilé android-library-base.gradle.kts

import COMPILE_SDK
import JAVA_VERSION
import JVM_TARGET
import MIN_SDK
import androidCoreKtx
import kotlinBundle

plugins {
    id("com.android.library")
}

android {
    compileSdk = COMPILE_SDK
    defaultConfig {
        minSdk = MIN_SDK
    }
    compileOptions {
        sourceCompatibility = JAVA_VERSION
        targetCompatibility = JAVA_VERSION
    }
    kotlinOptions {
        jvmTarget = JVM_TARGET
    }
}

dependencies {
    implementation(androidCoreKtx)
    implementation(kotlinBundle)
}

Tu remarques les constantes importées en haut du fichier — on y revient juste après.

Maintenant, tes modules Authentication et Database deviennent respectivement :

//build.gradle.kts du module Authentication avec plugin précompilé

plugins {
    id("precompiled_plugin.android-library-base")
}

android {
   namespace = "com.precompiledplugin.example.authentication"
}

dependencies {
    // Dépendances d'une bibliothèque permettant de faire de l'authentification 
    // ....
}
//build.gradle.kts du module Database avec plugin précompilé

plugins {
    id("precompiled_plugin.android-library-base")
}

android {
   namespace = "com.precompiledplugin.example.database"
}

dependencies {
     // Dépendances d'une bibliothèque permettant de faire de la base de donnée
    // ....
}

De 30 lignes répétées à 10 lignes essentielles. La configuration commune est centralisée, versionnée, maintenable. Ajouter un nouveau module prend désormais 2 minutes au lieu de 15.

Astuce : accéder au version catalog depuis un module précompilé

Depuis Gradle 7.4, le Version Catalog est la façon recommandée de gérer les dépendances d’un projet Android. Si tu ne l’utilises pas encore, tu passes à côté d’un gain de maintenabilité considérable — mais c’est un sujet pour un prochain article.

Le problème : par défaut, le Version Catalog n’est pas accessible depuis les plugins précompilés. C’est une limitation connue qui en décourage plus d’un.

Voici la solution — des fonctions d’extension qui permettent d’y accéder proprement. Crée l’arborescence buildSrc/src/main/kotlin/extensions/ et ajoute les fichiers suivants :

//Const.kts

internal const val catalogue_name = "libs"
//BundleExt.kts

import org.gradle.api.Project
import org.gradle.api.artifacts.ExternalModuleDependencyBundle
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.getByType

internal fun Project.bundle(key: String): Provider<ExternalModuleDependencyBundle> = extensions
    .getByType<VersionCatalogsExtension>()
    .named(catalogue_name)
    .findBundle(key)
    .get()

internal val Project.kotlinBundle get() = bundle("kotlinBundle")
internal val Project.kotlinTest get() = bundle("kotlinTest")
internal val Project.androidTest get() = bundle("androidTest")
internal val Project.androidXCompose get() = bundle("androidXCompose")
internal val Project.koinAndroidX get() = bundle("koinAndroidX")
internal val Project.koinAndroidBase get() = bundle("koinAndroidBase")
//LibrayExt.kt

import org.gradle.api.Project
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.getByType

internal fun Project.library(key: String): Provider<MinimalExternalModuleDependency> = extensions
    .getByType<VersionCatalogsExtension>()
    .named(catalogue_name)
    .findLibrary(key)
    .get()

internal val Project.androidCoreKtx get() = library("androidCoreKtx")
//VersionExt.kt

import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType

fun Project.version(key: String) = extensions
    .getByType<VersionCatalogsExtension>()
    .named(catalogue_name)
    .findVersion(key)
    .get()
    .requiredVersion

fun Project.versionInt(key: String) = version(key).toInt()

val JAVA_VERSION get() = JavaVersion.VERSION_11
val JVM_TARGET get() = "11"

val Project.MIN_SDK get() = versionInt("minSdk")
val Project.COMPILE_SDK get() = versionInt("compileSdk")

Ces extensions sont importées dans ton plugin précompilé — d’où les import en haut du fichier android-library-base.gradle.kts. Tu centralises les valeurs de version dans le catalog, tu y accèdes proprement depuis le plugin. Propre, maintenable, scalable.

Ce que j’aurais voulu savoir plus tôt

Les plugins Gradle précompilés, c’est l’une de ces solutions qu’on regrette de ne pas avoir adoptées plus tôt.

J’aurais pu économiser des dizaines d’heures sur des projets multi-modules si j’avais eu cet outil dès le départ. Aujourd’hui, c’est l’une des premières choses que je mets en place sur tout nouveau projet Android sérieux — et sur les projets KMP, c’est encore plus indispensable.

Combinés avec le Version Catalog, ils te donnent une configuration de build qui scale proprement, quel que soit le nombre de modules. Et ça, c’est exactement le genre de fondation qui fait la différence entre un projet qu’on peut maintenir pendant 5 ans et un projet qu’on redoute d’ouvrir le matin.

Le code complet est disponible dans la documentation officielle Gradle.

Tu utilises déjà les plugins précompilés dans tes projets ? Dis-moi en commentaire comment tu les as intégrés — les retours terrain sont toujours les plus intéressants.

Sanders

Si tu as aimé l'article, tu es libre de le partager! 🙂

2 Comments

  1. Maxime

    Hello !
    Top l’article ! 👌🏻
    Je vais essayer de mettre ça en place dans un mini projet perso 😉
    Merci 🙏🏻

    • Top. N’hésites pas si tu as des questions lors de la mise en place 😉

Laisser un commentaire