Écrire des plug-ins Gradle

Le plug-in Android Gradle (AGP) est le système de compilation officiel pour les applications Android. Il permet de compiler de nombreux types de sources différents et de les associer dans une application que vous pouvez exécuter sur un appareil Android physique ou un émulateur.

AGP contient des points d'extension permettant aux plug-ins de contrôler les entrées de compilation et d'étendre ses fonctionnalités via de nouvelles étapes qui peuvent être intégrées aux tâches de compilation standards. Les versions précédentes d'AGP ne disposaient pas d'API officielles clairement séparées des implémentations internes. À partir de la version 7.0, AGP propose un ensemble d'API officielles et stables sur lesquelles vous appuyer.

Cycle de vie des API AGP

AGP suit le cycle de vie des fonctionnalités Gradle pour désigner l'état de ses API :

  • Internal (Interne) : les API ne sont pas destinées à un usage public
  • Incubating (Incubation) : les API sont disponibles pour un usage public, mais non finales, ce qui signifie qu'elles peuvent ne pas être rétrocompatibles dans la version finale
  • Public : les API sont disponibles pour un usage public et stables
  • Deprecated (Obsolète) : les API sont ne sont plus prises en charge et remplacées par de nouvelles API

Règlement relatif aux abandons

AGP évolue avec l'abandon des anciennes API et leur remplacement par de nouvelles API stables et un nouveau langage spécifique au domaine (DSL). Cette évolution s'appliquera à plusieurs versions d'AGP. Pour en savoir plus, consultez la chronologie de migration de l'API et du DSL AGP.

Lorsque les API AGP sont obsolètes, pour cette migration ou non, elles restent disponibles dans la version majeure actuelle, mais génèrent des avertissements. Les API obsolètes seront complètement supprimées d'AGP dans la prochaine version majeure. Par exemple, si une API est obsolète dans AGP 7.0, elle est disponible dans cette version, mais génère des avertissements. Cette API ne sera plus disponible dans AGP 8.0.

Pour consulter des exemples de nouvelles API utilisées dans les personnalisations de compilation courantes, consultez les combinaisons de plug-ins Android Gradle. Elles fournissent des exemples de personnalisations de compilation courantes. Pour en savoir plus sur les nouvelles API, consultez notre documentation de référence.

Principes de base des compilations Gradle

Ce guide ne couvre pas l'intégralité du système de compilation Gradle. Il couvre cependant l'ensemble minimal de concepts nécessaires pour vous aider à intégrer nos API et renvoie à la documentation principale de Gradle pour en savoir plus.

Nous partons du principe que vous possédez des connaissances de base sur le fonctionnement de Gradle, y compris sur la configuration de projets, la modification de fichiers de compilation, l'application de plug-ins et l'exécution de tâches. Pour en savoir plus sur les bases de Gradle en lien avec AGP, nous vous recommandons de consulter Configurer votre build. Pour en savoir plus sur le framework général de personnalisation des plug-ins Gradle, consultez Développer des plug-ins Gradle personnalisés.

Glossaire des types différés Gradle

Gradle propose un certain nombre de types qui diffèrent des calculs intensifs ou une création Task aux phases ultérieures de la compilation. Ces types sont au cœur de nombreuses API Gradle et AGP. La liste suivante inclut les principaux types Gradle impliqués dans l'exécution différée, ainsi que leurs principales méthodes.

Provider<T>
Fournit une valeur de type T (où "T" désigne n'importe quel type) pouvant être lue pendant la phase d'exécution à l'aide de get() ou transformé en Provider<S> (où "S" désigne un autre type) à l'aide des méthodes map(), flatMap() et zip(). Notez que get() ne doit jamais être appelé lors de la phase de configuration.
  • map() : accepte un lambda et génère un Provider de type S, Provider<S>. L'argument lambda de map() utilise la valeur T et génère la valeur S. Le lambda n'est pas exécuté immédiatement. Son exécution est différée jusqu'à l'appel de get() sur le Provider<S> qui en résulte, ce qui diffère toute la chaîne.
  • flatMap() : accepte également un lambda et génère Provider<S>, mais le lambda accepte une valeur T et génère Provider<S> (plutôt que la valeur S directement). Utilisez flatMap() lorsque S ne peut pas être déterminé au moment de la configuration et que vous pouvez uniquement obtenir Provider<S>. En pratique, si vous avez utilisé map() et obtenu un type de résultat Provider<Provider<S>>, vous auriez probablement dû utiliser flatMap().
  • zip() : permet de combiner deux instances Provider pour générer un nouveau Provider, avec une valeur calculée à l'aide d'une fonction combinant les valeurs issues des deux instances Providers d'entrée.
Property<T>
Implémente Provider<T> et fournit donc une valeur de type T. Contrairement à Provider<T>, qui est en lecture seule, vous pouvez également définir une valeur pour Property<T>. Pour ce faire, vous disposez de deux méthodes :
  • Définissez directement une valeur de type T, si disponible, pour éviter des calculs différés.
  • Définissez un autre Provider<T> comme source de la valeur de Property<T>. Dans ce cas, la valeur T n'est matérialisée que lorsque la méthode Property.get() est appelée.
TaskProvider
Implémente Provider<Task>. Pour générer un TaskProvider, utilisez tasks.register() et non tasks.create(), afin de vous assurer que les tâches sont uniquement instanciées de manière différée lorsqu'elles sont nécessaires. Vous pouvez utiliser flatMap() pour accéder aux sorties d'uneTask avant que la Task ne soit créée, ce qui peut être utile si vous souhaitez utiliser les sorties comme entrées pour d'autres instances Task.

Les fournisseurs et leurs méthodes de transformation sont essentiels pour configurer les entrées et les sorties des tâches de manière différée, c'est-à-dire sans devoir créer toutes les tâches à l'avance ni résoudre les valeurs.

Les fournisseurs disposent également d'informations sur les dépendances des tâches. Lorsque vous créez un Provider en transformant une sortie de Task, cette Task devient une dépendance implicite de la Provider. Elle est créée et exécutée chaque fois que la valeur du Provider est résolue, par exemple lorsqu'une autre Task l'exige.

Voici un exemple d'enregistrement de deux tâches, GitVersionTask et ManifestProducerTask, qui diffèrent la création des instances Task jusqu'à ce qu'elles soient réellement requises. La valeur d'entrée ManifestProducerTask est définie sur un Provider issu de la sortie de GitVersionTask et donc, ManifestProducerTask dépend implicitement de GitVersionTask.

// Register a task lazily to get its TaskProvider.
val gitVersionProvider: TaskProvider =
    project.tasks.register("gitVersionProvider", GitVersionTask::class.java) {
        it.gitVersionOutputFile.set(
            File(project.buildDir, "intermediates/gitVersionProvider/output")
        )
    }

...

/**
 * Register another task in the configuration block (also executed lazily,
 * only if the task is required).
 */
val manifestProducer =
    project.tasks.register(variant.name + "ManifestProducer", ManifestProducerTask::class.java) {
        /**
         * Connect this task's input (gitInfoFile) to the output of
         * gitVersionProvider.
         */
        it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
    }

Ces deux tâches ne s'exécutent que si elles sont explicitement demandées. Cela peut intervenir lors d'un appel Gradle, par exemple si vous exécutez ./gradlew debugManifestProducer ou si la sortie de ManifestProducerTask est connectée à une autre tâche et que sa valeur est obligatoire.

Bien que vous écriviez des tâches personnalisées qui consomment des entrées et/ou génèrent des sorties, AGP n'offre pas directement d'accès public à ses propres tâches. Il s'agit d'un détail d'implémentation susceptible d'être modifié d'une version à une autre. Au lieu de cela, AGP propose l'API des variantes et l'accès à la sortie de ses tâches, ou des artefacts de compilation que vous pouvez lire et transformer. Pour plus d'informations, consultez la section API des variantes, artefacts et tâches de ce document.

Phases de compilation Gradle

La création d'un projet est un processus complexe et exigeant en termes de ressources. Il comporte différentes fonctionnalités telles que l'évitement de la configuration des tâches, des vérifications à jour et la fonctionnalité de mise en cache de la configuration, qui permettent de réduire le temps passé sur des calculs reproductibles ou inutiles.

Pour appliquer certaines de ces optimisations, les scripts et les plug-ins Gradle doivent respecter des règles strictes lors de chacune des phases de compilation Gradle : initialisation, configuration et exécution. Dans ce guide, nous allons nous concentrer sur les phases de configuration et d'exécution. Pour en savoir plus sur toutes les phases, consultez le Guide du cycle de vie des compilations Gradle.

Phase de configuration

Au cours de la phase de configuration, les scripts de compilation de tous les projets qui font partie de la compilation sont évalués, les plug-ins sont appliqués et les dépendances de compilation sont résolues. Cette phase permet de configurer la compilation à l'aide d'objets DSL et d'enregistrer les tâches et leurs entrées de manière différée.

La phase de configuration étant toujours active, quelle que soit la tâche demandée, il est particulièrement important qu'elle reste simple et limite tous les calculs en fonction des entrées autres que les scripts de compilation eux-mêmes. Autrement dit, vous ne devez pas exécuter de programmes externes ou lire des données à partir du réseau, ni effectuer de longs calculs qui peuvent être différés à la phase d'exécution en tant qu'instances Task appropriées.

Phase d'exécution

Lors de la phase d'exécution, les tâches demandées et les tâches dépendantes correspondantes sont exécutées. Plus précisément, la ou les méthodes de classe Task marquées @TaskAction sont exécutées. Lors de l'exécution d'une tâche, vous êtes autorisé à lire les entrées (telles que les fichiers) et à résoudre les fournisseurs différés en appelant Provider<T>.get(). Cette méthode permet de lancer une séquence d'appels map() ou flatMap() qui suivent les informations relatives aux dépendances des tâches contenues dans le fournisseur. Les tâches sont exécutées de manière différée pour matérialiser les valeurs requises.

API des variantes, artefacts et tâches

L'API des variantes est un mécanisme d'extension du plug-in Android Gradle vous permettant de manipuler les différentes options, normalement définies à l'aide du DSL dans les fichiers de configuration de compilation, qui ont une incidence sur la compilation Android. L'API des variantes vous donne également accès aux artefacts intermédiaires et finaux créés par la compilation, tels que les fichiers de classe, le fichier manifeste fusionné ou les fichiers APK/AAB.

Flux de compilation et points d'extension Android

Lorsque vous interagissez avec AGP, utilisez des points d'extension spécialement conçus plutôt que d'enregistrer des rappels de cycle de vie Gradle types (tels que afterEvaluate()) ou de configurer des dépendances Task explicites. Les tâches créées par AGP sont considérées comme des détails d'implémentation et ne sont pas exposées en tant qu'API publique. Vous devez éviter d'essayer d'obtenir des instances des objets Task ou de deviner les noms Task, et d'ajouter directement des rappels ou des dépendances à ces objets Task.

AGP suit les étapes ci-dessous pour créer et exécuter ses instances Task, ce qui génère les artefacts de compilation. Les principales étapes de création d'objets Variant sont suivies de rappels qui vous permettent de modifier certains objets créés pour une compilation. Il est important de noter que tous les rappels interviennent durant la phase de configuration (décrite sur cette page) et doivent être exécutés rapidement, en différant toute tâche complexe aux instances Task lors de la phase d'exécution.

  1. Analyse DSL : à ce stade, les scripts de compilation sont évalués et les différentes propriétés des objets DSL Android du bloc android sont créées et définies. Les rappels d'API des variantes décrits dans les sections suivantes sont également enregistrés au cours de cette phase.
  2. finalizeDsl() : rappel qui vous permet de modifier les objets DSL avant qu'ils ne soient verrouillés pour la création de composants (variantes). Les objets VariantBuilder sont créés à partir des données contenues dans les objets DSL.

  3. Verrouillage DSL : le DSL est maintenant verrouillé et les modifications ne sont plus possibles.

  4. beforeVariants() : ce rappel peut influer sur les composants créés ainsi que certaines de leurs propriétés via VariantBuilder. Il permet toujours de modifier le flux de compilation ainsi que les artefacts générés.

  5. Création des variantes : la liste des composants et des artefacts qui seront créés est maintenant finalisée et ne peut pas être modifiée.

  6. onVariants() : dans ce rappel, vous accédez aux objets Variant et pouvez définir des valeurs ou des fournisseurs pour les valeurs Property qu'ils contiennent à des fins de calcul différé.

  7. Verrouillage des variantes : les objets de variantes sont maintenant verrouillés et les modifications ne sont plus possibles.

  8. Tâches créées : les objets Variant et leurs valeurs Property permettent de créer les instances Task nécessaires à la compilation.

AGP introduit une extension AndroidComponentsExtension qui vous permet d'enregistrer des rappels pour finalizeDsl(), beforeVariants() et onVariants(). L'extension est disponible dans les scripts de compilation via le bloc androidComponents :

// This is used only for configuring the Android build through DSL.
android { ... }

// The androidComponents block is separate from the DSL.
androidComponents {
   finalizeDsl { extension ->
      ...
   }
}

Toutefois, nous vous recommandons de réserver les scripts de compilation pour la configuration déclarative à l'aide du DSL du bloc Android et de déplacer toute logique impérative personnalisée vers buildSrc ou les plug-ins externes. Vous pouvez également consulter les exemples buildSrc de notre dépôt GitHub de combinaisons Gradle pour apprendre à créer un plug-in dans votre projet. Voici un exemple d'enregistrement des rappels à partir du code du plug-in :

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            ...
        }
    }
}

Intéressons-nous de plus près aux rappels disponibles et au type de cas d'utilisation que votre plug-in peut prendre en charge dans chacun d'eux :

finalizeDsl(callback: (DslExtensionT) -> Unit)

Dans ce rappel, vous pouvez accéder aux objets DSL créés et les modifier en analysant les informations du bloc android dans les fichiers de compilation. Ces objets DSL sont utilisés pour initialiser et configurer les variantes dans les phases ultérieures de la compilation. Par exemple, vous pouvez créer de nouvelles configurations ou ignorer des propriétés par programmation, mais gardez à l'esprit que toutes les valeurs doivent être résolues au moment de la configuration et ne doivent donc pas dépendre d'entrées externes. Une fois l'exécution de ce rappel terminée, les objets DSL ne sont plus utiles et vous ne devez plus conserver de références à ceux-ci ni modifier leurs valeurs.

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            extension.buildTypes.create("extra").let {
                it.isJniDebuggable = true
            }
        }
    }
}

beforeVariants()

À ce stade de la compilation, vous avez accès aux objets VariantBuilder, qui déterminent les variantes qui seront créées, ainsi que leurs propriétés. Par exemple, vous pouvez désactiver par programmation certaines variantes, leurs tests, ou modifier uniquement la valeur d'une propriété (par exemple, minSdk) pour une variante choisie. Comme pour finalizeDsl(), toutes les valeurs que vous fournissez doivent être résolues au moment de la configuration et ne pas dépendre d'entrées externes. Les objets VariantBuilder ne doivent pas être modifiés une fois l'exécution du rappel beforeVariants() terminée.

androidComponents {
    beforeVariants { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

Le rappel beforeVariants() peut utiliser un objet VariantSelector qu'il vous est possible d'obtenir via la méthode selector() sur l'extension androidComponentsExtension. Vous pouvez l'utiliser pour filtrer les composants participant au rappel selon leur nom, leur type de compilation ou leur type de produit.

androidComponents {
    beforeVariants(selector().withName("adfree")) { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

onVariants()

Lors de l'appel de onVariants(), tous les artefacts créés par AGP sont déjà définis et vous ne pouvez donc plus les désactiver. Vous pouvez cependant modifier plusieurs valeurs utilisées pour les tâches en les définissant pour les attributs Property dans les objets Variant. Les valeurs Property n'étant résolues que lorsque les tâches AGP sont exécutées, vous pouvez les connecter en toute sécurité à des fournisseurs à partir de vos propres tâches personnalisées afin de procéder aux calculs nécessaires, y compris des lectures à partir d'éléments externes comme les fichiers ou le réseau.

// onVariants also supports VariantSelectors:
onVariants(selector().withBuildType("release")) { variant ->
    // Gather the output when we are in single mode (no multi-apk).
    val mainOutput = variant.outputs.single { it.outputType == OutputType.SINGLE }

    // Create version code generating task
    val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) {
        it.outputFile.set(project.layout.buildDirectory.file("${variant.name}/versionCode.txt"))
    }
    /**
     * Wire version code from the task output.
     * map() will create a lazy provider that:
     * 1. Runs just before the consumer(s), ensuring that the producer
     * (VersionCodeTask) has run and therefore the file is created.
     * 2. Contains task dependency information so that the consumer(s) run after
     * the producer.
     */
    mainOutput.versionCode.set(versionCodeTask.map { it.outputFile.get().asFile.readText().toInt() })
}

Ajouter des sources générées dans le build

Votre plug-in peut contribuer à plusieurs types de sources générées :

Pour obtenir la liste complète des sources que vous pouvez ajouter, consultez la section API Sources.

Cet extrait de code montre comment ajouter un dossier source personnalisé appelé ${variant.name} à l'ensemble de sources Java à l'aide de la fonction addStaticSourceDirectory(). La chaîne d'outils Android traitera ensuite ce dossier.

onVariants { variant ->
    variant.sources.java?.let { java ->
        java.addStaticSourceDirectory("custom/src/kotlin/${variant.name}")
    }
}

Pour en savoir plus, consultez la section addJavaSource.

Cet extrait de code montre comment ajouter à l'ensemble de sources res un répertoire contenant des ressources Android générées à partir d'une tâche personnalisée. Le processus est similaire pour les autres types de sources.

onVariants(selector().withBuildType("release")) { variant ->
    // Step 1. Register the task.
    val resCreationTask =
       project.tasks.register<ResCreatorTask>("create${variant.name}Res")

    // Step 2. Register the task output to the variant-generated source directory.
    variant.sources.res?.addGeneratedSourceDirectory(
       resCreationTask,
       ResCreatorTask::outputDirectory)
    }

...

// Step 3. Define the task.
abstract class ResCreatorTask: DefaultTask() {
   @get:OutputFiles
   abstract val outputDirectory: DirectoryProperty

   @TaskAction
   fun taskAction() {
      // Step 4. Generate your resources.
      ...
   }
}

Pour en savoir plus, consultez la section addCustomAsset.

Accéder aux artefacts et les modifier

En plus de vous offrir la possibilité de modifier les propriétés simples des objets Variant, AGP contient un mécanisme d'extension qui vous permet de lire ou de transformer les artefacts intermédiaires et finaux générés lors de la compilation. Par exemple, vous pouvez lire le contenu final du fichier AndroidManifest.xml fusionné dans une Task personnalisée pour l'analyser, ou remplacer tout son contenu par celui d'un fichier manifeste généré par votre Task personnalisée.

La liste des artefacts actuellement pris en charge est disponible dans la documentation de référence de la classe Artifact. Chaque type d'artefact possède certaines propriétés qu'il est utile de connaître :

Cardinalité

La cardinalité d'un Artifact représente son nombre d'instances FileSystemLocation, ou le nombre de fichiers ou de répertoires du type d'artefact. Vous pouvez obtenir des informations sur la cardinalité d'un artefact en vérifiant sa classe parente : les artefacts comportant un seul FileSystemLocation sont une sous-classe de Artifact.Single. Les artefacts comportant plusieurs instances FileSystemLocation constituent une sous-classe de Artifact.Multiple.

FileSystemLocation type

Vous pouvez vérifier si un Artifact représente des fichiers ou des répertoires en consultant son type FileSystemLocation paramétré, qui peut être un RegularFile ou un Directory.

Opérations disponibles

Chaque classe Artifact peut implémenter l'une des interfaces suivantes pour indiquer les opérations qu'elle prend en charge :

  • Transformable : permet d'utiliser un Artifact en tant qu'entrée d'une Task qui effectue des transformations arbitraires sur celui-ci et génère une nouvelle version de l'Artifact.
  • Appendable : s'applique uniquement aux artefacts correspondant à des sous-classes de Artifact.Multiple. Cela signifie que l'Artifact peut être ajouté ; une Task personnalisée peut créer des instances de ce type Artifact qui seront ajoutées à la liste existante.
  • Replaceable : s'applique uniquement aux artefacts correspondant à des sous-classes de Artifact.Single. Un Artifact remplaçable peut être remplacée par une nouvelle instance, générée en tant que sortie de Task.

En plus des trois opérations de modification d'artefact, chaque artefact prend en charge une opération get() (ou getAll()), qui renvoie un Provider avec la version finale de l'artefact (une fois toutes les opérations sur celui-ci terminées).

Plusieurs plug-ins peuvent ajouter autant d'opérations que nécessaire sur des artefacts du pipeline à partir du rappel onVariants(). AGP s'assurera de leur bon enchaînement afin que toutes les tâches s'exécutent au bon moment et que les artefacts soient correctement générés et mis à jour. Ainsi, lorsqu'une opération modifie des sorties en les ajoutant, les remplaçant ou les transformant, la version suivante de ces artefacts s'affiche en tant qu'entrées, et ainsi de suite.

Le point d'entrée d'enregistrement des opérations est la classe Artifacts. L'extrait de code suivant montre comment accéder à une instance Artifacts à partir d'une propriété de l'objet Variant dans le rappel onVariants().

Vous pouvez ensuite transmettre votre TaskProvider personnalisé pour obtenir un objet TaskBasedOperation (1), puis l'utiliser pour associer ses entrées et sorties à l'aide de l'une des méthodes wiredWith* (2).

La méthode exacte que vous devez choisir dépend de la cardinalité et du type FileSystemLocation implémenté par l'Artifact que vous souhaitez transformer.

Enfin, vous transmettez le type Artifact à une méthode représentant l'opération choisie sur l'objet *OperationRequest que vous obtenez en retour, par exemple toAppendTo(), toTransform() ou toCreate() (3).

androidComponents.onVariants { variant ->
    val manifestUpdater = // Custom task that will be used for the transform.
            project.tasks.register(variant.name + "ManifestUpdater", ManifestTransformerTask::class.java) {
                it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
            }
    // (1) Register the TaskProvider w.
    val variant.artifacts.use(manifestUpdater)
         // (2) Connect the input and output files.
        .wiredWithFiles(
            ManifestTransformerTask::mergedManifest,
            ManifestTransformerTask::updatedManifest)
        // (3) Indicate the artifact and operation type.
        .toTransform(SingleArtifact.MERGED_MANIFEST)
}

Dans cet exemple, MERGED_MANIFEST est un SingleArtifact correspondant à RegularFile. Dès lors, nous devons utiliser la méthode wiredWithFiles, qui accepte une seule référence RegularFileProperty pour l'entrée et une seule RegularFileProperty pour la sortie. D'autres méthodes wiredWith* sur la classe TaskBasedOperation fonctionnent pour d'autres combinaisons de cardinalité Artifact et types FileSystemLocation.

Pour en savoir plus sur l'extension d'AGP, nous vous recommandons de lire les sections suivantes du manuel du système de compilation Gradle :