Scrittura dei plug-in Gradle

Il plug-in Android per Gradle (AGP) è il sistema di compilazione ufficiale per le applicazioni Android. Include il supporto per la compilazione di molti tipi diversi di origini e il loro collegamento in un'applicazione che puoi eseguire su un dispositivo Android fisico o un emulatore.

AGP contiene punti di estensione per i plug-in per controllare gli input di compilazione ed estendere la relativa funzionalità tramite nuovi passaggi che possono essere integrati con le attività di compilazione standard. Le versioni precedenti di AGP non avevano API ufficiali chiaramente separate dalle implementazioni interne. A partire dalla versione 7.0, AGP dispone di un insieme di API ufficiali e stabili su cui puoi fare affidamento.

Ciclo di vita dell'API AGP

AGP segue il ciclo di vita delle funzionalità di Gradle per designare lo stato delle sue API:

  • Interna: non destinata all'uso pubblico
  • In fase di incubazione: disponibili per l'uso pubblico, ma non definitive, il che significa che potrebbero non essere compatibili con le versioni precedenti nella versione finale
  • Pubblico: disponibile per uso pubblico e stabile
  • Ritiro: non più supportate e sostituite da nuove API

Norme sul ritiro

AGP è in evoluzione con il ritiro delle vecchie API e la loro sostituzione con API nuove e stabili e un nuovo linguaggio DSL (Domain Specific Language). Questa evoluzione riguarderà più release di AGP e puoi scoprire di più consultando le tempistiche della migrazione dell'API/DSL di AGP.

Quando le API AGP vengono ritirate, per questa migrazione o per altri motivi, continueranno a essere disponibili nella release principale corrente, ma genereranno avvisi. Le API ritirate verranno rimosse completamente dall'AGP nella release principale successiva. Ad esempio, se un'API viene ritirata in AGP 7.0, sarà disponibile in quella versione e genererà avvisi. Questa API non sarà più disponibile in AGP 8.0.

Per vedere esempi di nuove API utilizzate nelle personalizzazioni di build comuni, dai un'occhiata alle ricette del plug-in Gradle per Android. Forniscono esempi di personalizzazioni di compilazione comuni. Puoi anche trovare maggiori dettagli sulle nuove API nella nostra documentazione di riferimento.

Nozioni di base sulla compilazione di Gradle

Questa guida non copre l'intero sistema di compilazione di Gradle. Tuttavia, copre l'insieme minimo di concetti necessari per aiutarti a eseguire l'integrazione con le nostre API e rimanda alla documentazione principale di Gradle per ulteriori letture.

Presumiamo una conoscenza di base del funzionamento di Gradle, inclusa la configurazione dei progetti, la modifica dei file di build, l'applicazione dei plug-in e l'esecuzione delle attività. Per conoscere le nozioni di base di Gradle rispetto ad AGP, ti consigliamo di consultare la sezione Configurare la compilazione. Per informazioni sul framework generale per la personalizzazione dei plug-in Gradle, consulta Sviluppare plug-in Gradle personalizzati.

Glossario dei tipi lazy di Gradle

Gradle offre una serie di tipi che si comportano in modo "lazy" o che consentono di rimandare computazioni pesanti o la creazione di Task alle fasi successive della compilazione. Questi tipi sono alla base di molte API Gradle e AGP. L'elenco seguente include i principali tipi di Gradle coinvolti nell'esecuzione lazy e i relativi metodi chiave.

Provider<T>
Fornisce un valore di tipo T (dove "T" indica un tipo qualsiasi), che può essere letto durante la fase di esecuzione utilizzando get() o trasformato in un nuovo Provider<S> (dove "S" indica un altro tipo) utilizzando i metodi map(), flatMap() e zip(). Tieni presente che get() non deve essere chiamato mai durante la fase di configurazione.
  • map(): accetta una funzione lambda e produce un Provider di tipo S, Provider<S>. L'argomento lambda di map() assume il valore T e produce il valore S. La funzione lambda non viene eseguita immediatamente, ma la sua esecuzione viene posticipata al momento in cui viene chiamata get() nel Provider<S> risultante, rendendo tutta la catena lazy.
  • flatMap(): accetta anche una funzione lambda e produce Provider<S>, ma la funzione lambda prende un valore T e produce Provider<S> (anziché produrre direttamente il valore S). Utilizza flatMap() quando S non può essere determinata al momento della configurazione e puoi ottenere solo Provider<S>. In pratica, se hai utilizzato map() e hai ottenuto un tipo di risultato Provider<Provider<S>>, probabilmente dovresti utilizzare flatMap().
  • zip(): consente di combinare due istanze Provider per produrre un nuovo Provider, con un valore calcolato utilizzando una funzione che combina i valori delle due istanze Providers di input.
Property<T>
Implementa Provider<T>, quindi fornisce anche un valore di tipo T. A differenza di Provider<T>, che è di sola lettura, puoi anche impostare un valore per Property<T>. Puoi farlo in due modi:
  • Imposta un valore di tipo T direttamente quando è disponibile, senza la necessità di calcoli differiti.
  • Imposta un altro Provider<T> come origine del valore di Property<T>. In questo caso, il valore T viene materializzato solo quando viene chiamato Property.get().
TaskProvider
Implementa Provider<Task>. Per generare un TaskProvider, utilizza tasks.register() e non tasks.create(), per assicurarti che le attività vengano messe in istanza solo in modo lazy quando sono necessarie. Puoi utilizzare flatMap() per accedere agli output di un Task prima che venga creato, il che può essere utile se vuoi utilizzare gli output come input per altre istanze Task.Task

I provider e i relativi metodi di trasformazione sono essenziali per configurare gli input e gli output delle attività in modo lazy, ovvero senza dover creare tutte le attività in anticipo e risolvere i valori.

I fornitori forniscono anche informazioni sulle dipendenze delle attività. Quando crei un Provider trasformando un output Task, quest'ultimo diventa una dipendenza implicita del Provider e verrà creato ed eseguito ogni volta che il valore del Provider viene risolto, ad esempio quando un altro Provider lo richiede.TaskTask

Ecco un esempio di registrazione di due attività, GitVersionTask e ManifestProducerTask, mentre la creazione delle istanze Task viene posticipata fino a quando non sono effettivamente necessarie. Il valore di input ManifestProducerTask è impostato su un valore Provider ottenuto dall'output di GitVersionTask, quindi ManifestProducerTask dipende implicitamente da 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))
    }

Queste due attività verranno eseguite solo se richieste esplicitamente. Questo può accadere nell'ambito di un'invocazione di Gradle, ad esempio se esegui ./gradlew debugManifestProducer o se l'output di ./gradlew debugManifestProducer è collegato a un'altra attività e il relativo valore diventa obbligatorio.ManifestProducerTask

Sebbene tu debba scrivere attività personalizzate che consumano input e/o producono output, AGP non offre direttamente l'accesso pubblico alle proprie attività. Si tratta di un dettaglio di implementazione soggetto a modifiche da una versione all'altra. AGP offre invece l'API Variant e l'accesso all'output delle sue attività, o elementi di compilazione, che puoi leggere e trasformare. Per ulteriori informazioni, consulta la sezione API, artefatti e attività Variant in questo documento.

Fasi di compilazione di Gradle

La creazione di un progetto è un processo intrinsecamente complicato e che richiede molte risorse. Esistono varie funzionalità, come l'evitare la configurazione delle attività, i controlli aggiornati e la funzionalità di memorizzazione nella cache della configurazione, che contribuiscono a ridurre al minimo il tempo impiegato per i calcoli riproducibili o non necessari.

Per applicare alcune di queste ottimizzazioni, gli script e i plug-in di Gradle devono rispettare regole rigorose durante ciascuna delle distinte fasi di compilazione di Gradle: inizializzazione, configurazione ed esecuzione. In questa guida, ci concentreremo sulle fasi di configurazione ed esecuzione. Puoi trovare ulteriori informazioni su tutte le fasi nella guida al ciclo di vita della build di Gradle.

Fase di configurazione

Durante la fase di configurazione, vengono valutati gli script di compilazione di tutti i progetti che fanno parte della compilazione, vengono applicati i plug-in e vengono risolte le dipendenze di compilazione. Questa fase deve essere utilizzata per configurare la compilazione utilizzando oggetti DSL e per registrare le attività e i relativi input in modo lazy.

Poiché la fase di configurazione viene eseguita sempre, indipendentemente dall'attività richiesta, è particolarmente importante mantenerla snella e limitare qualsiasi calcolo in modo che non dipenda da input diversi dagli script di compilazione stessi. In altre parole, non devi eseguire programmi esterni o leggere dalla rete o eseguire calcoli lunghi che possono essere differiti alla fase di esecuzione come istanze Task appropriate.

Fase di esecuzione

Nella fase di esecuzione vengono eseguite le attività richieste e le relative attività dipendenti. Nello specifico, vengono eseguiti i metodi della classe Task contrassegnati con @TaskAction. Durante l'esecuzione delle attività, puoi leggere dagli input (ad esempio i file) e risolvere i provider lazy chiamando Provider<T>.get(). La risoluzione dei fornitori lazzati in questo modo avvia una sequenza di chiamate map() o flatMap() che seguono le informazioni sulle dipendenze delle attività contenute nel provider. Le attività vengono eseguite in modo lazy per materializzare i valori richiesti.

API, artefatti e attività Variant

L'API Variant è un meccanismo di estensione nel plug-in Gradle per Android che consente di manipolare le varie opzioni, normalmente impostate utilizzando il DSL nei file di configurazione della build, che influiscono sulla build di Android. L'API Variant consente inoltre di accedere agli elementi intermedi e finali creati dalla compilazione, come i file di classe, il manifest unito o i file APK/AAB.

Flusso di compilazione di Android e punti di estensione

Quando interagisci con AGP, utilizza punti di estensione appositamente realizzati anziché registrare i tipici callback del ciclo di vita di Gradle (ad esempio afterEvaluate()) o configurare dipendenze Task esplicite. Le attività create da AGP sono considerate dettagli di implementazione e non sono esposte come API pubblica. Devi evitare di provare a ottenere istanze degli oggetti Task o di indovinare i nomi Task e di aggiungere direttamente callback o dipendenze a questi oggetti Task.

AGP completa i seguenti passaggi per creare ed eseguire le sue istanze Task, che a loro volta producono gli artefatti di compilazione. I passaggi principali per la creazione di oggetti Variant sono seguiti da callback che ti consentono di apportare modifiche a determinati oggetti creati nell'ambito di una compilazione. È importante notare che tutti i callback vengono eseguiti durante la fase di configurazione (descritta in questa pagina) e devono essere eseguiti rapidamente, rimandando invece eventuali operazioni complesse alle istanze Task appropriate durante la fase di esecuzione.

  1. Analisi del codice DSL: viene eseguita quando vengono valutati i build script e vengono create e impostate le varie proprietà degli oggetti DSL Android del blocco android. Anche i callback dell'API Variant descritti nelle sezioni seguenti vengono registrati durante questa fase.
  2. finalizeDsl(): callback che consente di modificare gli oggetti DSL prima che vengano bloccati per la creazione di componenti (varianti). Gli oggetti VariantBuilder vengono creati in base ai dati contenuti negli oggetti DSL.

  3. Blocco DSL: la connessione DSL è ora bloccata e non sono più possibili modifiche.

  4. beforeVariants(): questo callback può influire sui componenti creati e su alcune delle relative proprietà tramite VariantBuilder. Consente comunque di apportare modifiche al flusso di compilazione e agli elementi prodotti.

  5. Creazione di varianti: l'elenco dei componenti e degli elementi che verranno creati è ora finalizzato e non può essere modificato.

  6. onVariants(): in questo callback, hai accesso agli oggetti Variant creati e puoi impostare valori o provider per i valori Property che contengono, in modo che vengano calcolati in modo lazy.

  7. Blocco delle varianti: gli oggetti delle varianti sono ora bloccati e le modifiche non sono più possibili.

  8. Attività create: gli oggetti Variant e i relativi valori Property vengono utilizzati per creare le istanze Task necessarie per eseguire la compilazione.

AGP introduce un AndroidComponentsExtension che consente di registrare callback per finalizeDsl(), beforeVariants() e onVariants(). L'estensione è disponibile negli script di compilazione tramite il blocco androidComponents:

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

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

Tuttavia, ti consigliamo di conservare gli script di compilazione solo per la configurazione dichiarativa utilizzando il DSL del blocco android e di spostare qualsiasi logica imperativa personalizzata in buildSrc o in plug-in esterni. Puoi anche dare un'occhiata ai buildSrc samples nel nostro repository GitHub delle ricette Gradle per scoprire come creare un plug-in nel tuo progetto. Ecco un esempio di registrazione dei callback dal codice del plug-in:

abstract class ExamplePlugin: Plugin<Project> {

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

Esaminiamo più da vicino i callback disponibili e il tipo di casi d'uso che il tuo plug-in può supportare in ciascuno di essi:

finalizeDsl(callback: (DslExtensionT) -> Unit)

In questo callback puoi accedere e modificare gli oggetti DSL che sono stati creati analizzando le informazioni del blocco android nei file di compilazione. Questi oggetti DSL verranno utilizzati per inizializzare e configurare le varianti nelle fasi successive della compilazione. Ad esempio, puoi creare programmaticamente nuove configurazioni o sostituire le proprietà, ma tieni presente che tutti i valori devono essere risolti al momento della configurazione, quindi non devono fare affidamento su input esterni. Al termine dell'esecuzione di questo callback, gli oggetti DSL non sono più utili e non devi più conservare riferimenti a questi oggetti o modificarne i valori.

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()

In questa fase della compilazione, puoi accedere agli oggetti VariantBuilder, che determinano le varianti che verranno create e le relative proprietà. Ad esempio, puoi disattivare programmaticamente determinate varianti, i relativi test o modificare il valore di una proprietà (ad esempio minSdk) solo per una variante scelta. Come per finalizeDsl(), tutti i valori forniti devono essere risolti al momento della configurazione e non dipendere da input esterni. Gli oggetti VariantBuilder non devono essere modificati al termine dell'esecuzione del callback beforeVariants().

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

Il callback beforeVariants() può facoltativamente accettare un VariantSelector, che puoi ottenere tramite il metodo selector() su androidComponentsExtension. Puoi utilizzarlo per filtrare i componenti che partecipano all'invocazione del callback in base al nome, al tipo di build o alla versione del prodotto.

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

onVariants()

Al momento dell'esecuzione della chiamata a onVariants(), tutti gli elementi che verranno creati da AGP sono già stati decisi, quindi non puoi più disattivarli. Tuttavia, puoi modificare alcuni dei valori utilizzati per le attività impostandoli per gli attributi Property negli oggetti Variant. Poiché i valori Property vengono risolti solo quando vengono eseguite le attività dell'AGP, puoi collegarli in sicurezza ai fornitori dalle tue attività personalizzate che eseguiranno tutti i calcoli richiesti, inclusa la lettura da input esterni come file o la rete.

// 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() })
}

Contribuire con le origini generate alla compilazione

Il tuo plug-in può contribuire con alcuni tipi di origini generate, ad esempio:

Per l'elenco completo delle origini che puoi aggiungere, consulta l'API Sources.

Questo snippet di codice mostra come aggiungere una cartella di origine personalizzata denominata ${variant.name} al set di origine Java utilizzando la funzione addStaticSourceDirectory(). La toolchain Android elabora quindi questa cartella.

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

Per ulteriori dettagli, consulta la ricetta addJavaSource.

Questo snippet di codice mostra come aggiungere una directory con risorse Android generate da un'attività personalizzata al set di origine res. La procedura è simile per altri tipi di origini.

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.
      ...
   }
}

Per ulteriori dettagli, consulta la ricetta addCustomAsset.

Accedere e modificare gli elementi

Oltre a consentirti di modificare proprietà semplici sugli oggetti Variant, AGP contiene anche un meccanismo di estensione che ti consente di leggere o trasformare gli elementi intermedi e finali prodotti durante la compilazione. Ad esempio, puoi leggere i contenuti finali del file AndroidManifest.xml unito in un Task personalizzato per analizzarlo oppure puoi sostituirne i contenuti interamente con quelli di un file manifest generato dal tuo Task personalizzato.

Puoi trovare l'elenco degli elementi attualmente supportati nella documentazione di riferimento per la classe Artifact. Ogni tipo di artefatto ha determinate proprietà utili da conoscere:

Cardinalità

La cardinalità di un Artifact rappresenta il numero di istanze FileSystemLocation o il numero di file o directory del tipo di elemento. Puoi ottenere informazioni sulla cardinalità di un elemento controllandone la classe principale: gli elementi con un singolo FileSystemLocation saranno una sottoclasse di Artifact.Single; gli elementi con più istanze di FileSystemLocation saranno una sottoclasse di Artifact.Multiple.

FileSystemLocation tipo

Puoi verificare se un Artifact rappresenta file o directory controllandone il tipo FileSystemLocation parametro, che può essere RegularFile o Directory.

Operazioni supportate

Ogni classe Artifact può implementare una delle seguenti interfacce per indicare le operazioni supportate:

  • Transformable: consente di utilizzare un Artifact come input per un Task che esegue trasformazioni arbitrarie e genera una nuova versione del Artifact.
  • Appendable: si applica solo agli elementi che sono sottoclassi di Artifact.Multiple. Ciò significa che è possibile aggiungere elementi a Artifact, ovvero un Task personalizzato può creare nuove istanze di questo tipo di Artifact che verranno aggiunte all'elenco esistente.
  • Replaceable: si applica solo agli elementi che sono sottoclassi di Artifact.Single. Un Artifact sostituibile può essere sostituito da un'istanza completamente nuova, prodotta come output di un Task.

Oltre alle tre operazioni di modifica degli elementi, ogni elemento supporta un'operazione get() (o getAll()), che restituisce un Provider con la versione finale dell'elemento (dopo il completamento di tutte le operazioni su di esso).

Più plug-in possono aggiungere un numero qualsiasi di operazioni sugli elementi nella pipeline dal callback onVariants() e AGP si assicurerà che vengano collegati correttamente in modo che tutte le attività vengano eseguite al momento giusto e che gli elementi vengano prodotti e aggiornati correttamente. Ciò significa che quando un'operazione modifica gli output aggiungendoli, sostituendoli o trasformandoli, l'operazione successiva vedrà la versione aggiornata di questi elementi come input e così via.

Il punto di ingresso per le operazioni di registrazione è la classe Artifacts. Il seguente snippet di codice mostra come accedere a un'istanza di Artifacts da una proprietà dell'oggetto Variant nel callback onVariants().

Puoi quindi passare il tuo TaskProvider personalizzato per ottenere un oggetto TaskBasedOperation (1) e utilizzarlo per collegare le relative entrate e uscite utilizzando uno dei metodi wiredWith* (2).

Il metodo esatto da scegliere dipende dalla cardinalità e dal tipo di FileSystemLocation implementato dal Artifact che vuoi trasformare.

Infine, passi il tipo Artifact a un metodo che rappresenta l'operazione scelta sull'oggetto *OperationRequest che ricevi in cambio, ad esempio toAppendTo(), toTransform() o 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)
}

In questo esempio, MERGED_MANIFEST è un SingleArtifact ed è un RegularFile. Per questo motivo, dobbiamo utilizzare il metodo wiredWithFiles, che accetta un singolo riferimento RegularFileProperty per l'input e un singolo RegularFileProperty per l'output. Esistono altri metodi wiredWith* nella classe TaskBasedOperation che funzionano per altre combinazioni di cardinalità Artifact e tipi FileSystemLocation.

Per scoprire di più sull'estensione di AGP, ti consigliamo di leggere le seguenti sezioni del manuale del sistema di compilazione Gradle: