Programar plug-ins do Gradle

O Plug-in do Android para Gradle (AGP, na sigla em inglês) é o sistema de build oficial para apps Android. Ele inclui compatibilidade para compilar vários tipos diferentes de fontes e vinculá-las a um aplicativo que pode ser executado em um dispositivo Android físico ou um emulador.

O AGP contém pontos de extensão para plug-ins que controlam as entradas de build e estendem a funcionalidade por meio de novas etapas que podem ser integradas a tarefas de build padrão. As versões anteriores do AGP não tinham APIs oficiais claramente separadas de implementações internas. A partir da versão 7.0, o AGP tem um conjunto de APIs oficiais e estáveis confiáveis.

Ciclo de vida da API AGP

O AGP segue o ciclo de vida do recurso do Gradle (link em inglês) para designar o estado das APIs:

  • Interna: não se destina ao uso público
  • Incubação: disponível para uso público, mas não o final, o que significa que pode ser incompatível com versões anteriores na versão final.
  • Pública: disponível para uso público e estável
  • Descontinuada: não é mais compatível e foi substituída por novas APIs

Política de descontinuação

O AGP está evoluindo com a descontinuação de APIs antigas e a substituição delas por APIs novas e estáveis e uma nova Linguagem específica de domínio (DSL, na sigla em inglês). Essa evolução abrangerá várias versões do AGP. Saiba mais sobre isso no cronograma de migração da DSL/API AGP.

Quando as APIs AGP forem descontinuadas, nessa migração ou de outra forma, elas continuarão disponíveis na versão principal atual, mas gerarão avisos. As APIs descontinuadas serão totalmente removidas do AGP na próxima versão principal. Por exemplo, se uma API tiver sido descontinuada no AGP 7.0, ela estará disponível nessa versão e gerará avisos. Essa API não estará mais disponível no AGP 8.0.

Para ver exemplos de novas APIs usadas em personalizações de build comuns, consulte as receitas do Plug-in do Android para Gradle (link em inglês). Elas fornecem exemplos de personalizações de build comuns. Você também encontra mais detalhes sobre as novas APIs na nossa documentação de referência.

Conceitos básicos de build do Gradle

Este guia não abrange todo o sistema de build do Gradle. No entanto, aborda o conjunto mínimo de conceitos necessários para ajudar você a se integrar às nossas APIs e vincula a documentação principal do Gradle para mais leitura.

Presumimos um conhecimento básico sobre como o Gradle funciona, incluindo como configurar projetos, editar arquivos de build, aplicar plug-ins e executar tarefas. Para saber mais sobre os conceitos básicos do Gradle em relação ao AGP, recomendamos que você consulte Configurar seu build. Para saber mais sobre a estrutura geral de personalização de plug-ins do Gradle, consulte Desenvolvimento de plug-ins personalizados do Gradle (link em inglês).

Glossário de tipos lentos do Gradle

O Gradle oferece vários tipos que se comportam "lentamente" ou ajudam a adiar cálculos complexos ou a criação de Task (link em inglês) para fases posteriores do build. Esses tipos são a base de muitas APIs AGP e Gradle. A lista a seguir inclui os principais tipos do Gradle envolvidos na execução lenta e os respectivos métodos principais (links em inglês).

Provider<T>
Fornece um valor do tipo T (em que "T" significa qualquer tipo), que pode ser lido durante a fase de execução usando get() ou transformado em um novo Provider<S> (em que "S" significa outro tipo) usando os métodos map(), flatMap() e zip(). Observe que get() nunca pode ser chamado durante a fase de configuração.
  • map(): aceita um lambda e produz um Provider do tipo S, Provider<S>. O argumento lambda para map() usa o valor T e produz o valor S. O lambda não é executado imediatamente. Em vez disso, a execução é adiada para o momento em que o get() é chamado no Provider<S> resultante, tornando toda a cadeia lenta.
  • flatMap(): também aceita um lambda e produz Provider<S>, mas o lambda usa um valor T e produz Provider<S>, em vez de produzir o valor S diretamente. Use flatMap() quando S não puder ser determinado no momento da configuração e for possível conseguir apenas Provider<S>. Na prática, se você usou map() e teve um tipo de resultado Provider<Provider<S>>, isso provavelmente significa que você usou flatMap().
  • zip(): permite combinar duas instâncias Provider para produzir um novo Provider, com um valor calculado usando uma função que combina os valores das duas instâncias de Providers de entrada.
Property<T>
Implementa o Provider<T>, assim também fornece um valor do tipo T. Diferentemente do Provider<T>, que é somente leitura, também é possível definir um valor para Property<T>. Há duas maneiras de fazer isso:
  • Defina um valor do tipo T diretamente quando ele estiver disponível, sem a necessidade de cálculos adiados.
  • Defina outro Provider<T> como origem do valor de Property<T>. Nesse caso, o valor T é materializado apenas quando Property.get() é chamado.
TaskProvider
Implementa Provider<Task>. Para gerar um TaskProvider, use tasks.register(), e não tasks.create(), para garantir que as tarefas só sejam instanciadas lentamente quando forem necessárias. É possível usar flatMap() para acessar as saídas de uma Task antes que a Task seja criada, o que poderá ser útil se você quiser usar as saídas como entradas para outras instâncias de Task.

Os provedores e os métodos de transformação deles são essenciais para configurar entradas e saídas de tarefas de maneira lenta, ou seja, sem a necessidade de criar todas as tarefas antecipadamente e resolver os valores.

Os provedores também transportam informações de dependência de tarefas. Quando você cria um Provider transformando a saída de uma Task, aquela Task se torna uma dependência implícita do Provider e será criada e executada sempre que o valor do Provider for resolvido, como quando for necessário para outra Task.

Veja um exemplo de registro de duas tarefas, GitVersionTask e ManifestProducerTask, enquanto a criação das instâncias de Task é adiada até que elas sejam realmente necessárias. O valor de entrada de ManifestProducerTask é definido como um Provider recebido da saída de GitVersionTask. Portanto, ManifestProducerTask depende implicitamente 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))
    }

Essas duas tarefas só serão executadas se forem explicitamente solicitadas. Isso pode acontecer como parte de uma invocação do Gradle, por exemplo, se você executar ./gradlew debugManifestProducer ou se a saída de ManifestProducerTask estiver conectada a alguma outra tarefa e o valor dela se tornar obrigatório.

Embora você grave tarefas personalizadas que consomem entradas e/ou produzem saídas, o AGP não oferece acesso público às próprias tarefas diretamente. Elas são um detalhe de implementação sujeito a mudanças de uma versão para outra. Em vez disso, o AGP oferece a API Variant e o acesso à saída das tarefas, ou artefatos de build, que podem ser lidos e transformados. Consulte API Variant, artefatos e tarefas neste documento para mais informações.

Fases de build do Gradle

Criar um projeto é inerentemente um processo complexo e que exige recursos, e há vários recursos, como prevenção da configuração de tarefas, verificações atualizadas e o recurso de armazenamento em cache da configuração, que ajudam a minimizar o tempo gasto em cálculos reproduzíveis ou desnecessários.

Para aplicar algumas dessas otimizações, os scripts e plug-ins do Gradle precisam obedecer regras rígidas durante cada uma das fases de build do Gradle: inicialização, configuração e execução. Neste guia, falaremos sobre as fases de configuração e execução. Você pode encontrar mais informações sobre todas as fases no guia do ciclo de vida de build do Gradle (link em inglês).

Fase de configuração

Durante a fase de configuração, os scripts de build de todos os projetos que fazem parte do build são avaliados, os plug-ins são aplicados e as dependências de build são resolvidas. Essa fase precisa ser usada para configurar o build usando objetos DSL e para registrar as tarefas e as entradas delas lentamente.

Como a fase de configuração sempre é executada, independentemente de qual tarefa for solicitada para execução, é importante mantê-la enxuta e evitar que os cálculos dependam de entradas que não sejam os próprios scripts de build. Ou seja, não execute programas externos, não leia pela rede nem execute cálculos longos que possam ser adiados para a fase de execução como instâncias de Task adequadas.

Fase de execução

Na fase de execução, as tarefas solicitadas e as tarefas dependentes são executadas. Especificamente, os métodos de classe Task marcados com @TaskAction são executados. Durante a execução da tarefa, é possível ler as entradas (como arquivos) e resolver provedores lentos chamando Provider<T>.get(). Resolver provedores lentos dessa maneira inicia uma sequência de chamadas map() ou flatMap() que seguem as informações de dependência de tarefa contidas no provedor. As tarefas são executadas lentamente para materializar os valores necessários.

API Variant, artefatos e tarefas

A API Variant é um mecanismo de extensão no Plug-in do Android para Gradle que permite manipular as várias opções, normalmente definidas usando a DSL em arquivos de configuração do build, que influenciam o build do Android. A API Variant também oferece acesso a artefatos intermediários e finais criados pelo build, como arquivos de classe, o manifesto integrado ou arquivos APK/AAB.

Pontos de extensão e fluxo de build do Android

Ao interagir com o AGP, use pontos de extensão especialmente criados em vez de registrar callbacks do ciclo de vida comuns do Gradle (como afterEvaluate()) ou configurar dependências Task explícitas. As tarefas criadas pelo AGP são consideradas detalhes de implementação e não são expostas como uma API pública. Evite tentar receber instâncias dos objetos Task ou adivinhar os nomes de Task e adicionar diretamente callbacks ou dependências a esses objetos Task.

O AGP conclui as etapas a seguir para criar e executar as instâncias de Task, que, por sua vez, produzem os artefatos de build. As principais etapas envolvidas na criação do objeto Variant são seguidas por callbacks que permitem fazer mudanças em determinados objetos criados como parte de um build. É importante observar que todos os callbacks ocorrem durante a fase de configuração (descrita nesta página) e precisam ser executados rapidamente, adiando qualquer trabalho complicado para instâncias de Task adequadas durante a fase de execução.

  1. Análise DSL: é quando os scripts de build são avaliados e as várias propriedades dos objetos DSL do Android do bloco android são criadas e definidas. Os callbacks da API Variant descritos nas seções a seguir também são registrados durante essa fase.
  2. finalizeDsl(): callback que permite mudar os objetos DSL antes de serem bloqueados para a criação do componente (variante). Objetos VariantBuilder são criados com base nos dados contidos nos objetos DSL.

  3. Bloqueio de DSL: agora a DSL está bloqueada e as mudanças não são mais possíveis.

  4. beforeVariants(): esse callback pode influenciar quais componentes são criados e algumas propriedades deles, usando VariantBuilder. Ele ainda permite modificações no fluxo de build e nos artefatos produzidos.

  5. Criação de variantes: a lista de componentes e artefatos que serão criados agora está finalizada e não pode ser modificada.

  6. onVariants(): nesse callback, você tem acesso aos objetos Variant criados e pode definir valores ou provedores para os valores Property que eles contêm, para serem calculados lentamente.

  7. Bloqueio de variantes: os objetos de variante agora estão bloqueados, e as mudanças não são mais possíveis.

  8. Tarefas criadas: objetos Variant e seus valores Property são usados para criar as instâncias de Task necessárias para executar o build.

O AGP introduz uma AndroidComponentsExtension que permite registrar callbacks para finalizeDsl(), beforeVariants() e onVariants(). A extensão está disponível em scripts de build por meio do bloco androidComponents:

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

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

No entanto, recomendamos manter os scripts de build somente para configuração declarativa usando a DSL do bloco android e mover qualquer lógica imperativa personalizada para buildSrc (link em inglês) ou plug-ins externos. Você também pode dar uma olhada nos exemplos de buildSrc (link em inglês) no nosso repositório GitHub de receitas do Gradle para saber como criar um plug-in no seu projeto. Veja um exemplo de como registrar os callbacks pelo código do plug-in:

abstract class ExamplePlugin: Plugin<Project> {

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

Veja com mais detalhes os callbacks disponíveis e os tipos de casos de uso compatíveis com seu plug-in em cada um deles:

finalizeDsl(callback: (DslExtensionT) -> Unit)

Nesse callback, você pode acessar e modificar os objetos DSL que foram criados analisando as informações do bloco android nos arquivos de build. Esses objetos DSL serão usados para inicializar e configurar variantes em fases posteriores do build. Por exemplo, é possível criar novas configurações ou modificar propriedades de maneira programática, mas lembre-se de que todos os valores precisam ser resolvidos no momento da configuração. Portanto, eles não podem depender de entradas externas. Após a execução desse callback, os objetos DSL não são mais úteis e você não precisa mais manter referências a eles ou modificar os valores.

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

Nesse estágio do build, você tem acesso aos objetos VariantBuilder, que determinam as variantes que serão criadas e as propriedades delas. Por exemplo, é possível desativar de maneira programática determinadas variantes e os testes delas ou mudar o valor de uma propriedade (por exemplo, minSdk) somente para uma variante escolhida. De modo semelhante a finalizeDsl(), todos os valores fornecidos precisam ser resolvidos no momento da configuração e não dependem de entradas externas. Os objetos VariantBuilder não podem ser modificados depois que a execução do callback beforeVariants() é concluída.

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

O callback beforeVariants() pode ter VariantSelector, que você pode receber por meio do método selector() na androidComponentsExtension. É possível usá-lo para filtrar componentes que participam da invocação do callback com base no nome, tipo de build ou variação de produto.

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

onVariants()

No momento em que onVariants() é chamado, todos os artefatos que serão criados pelo AGP já estão decididos, portanto, não é mais possível desativá-los. No entanto, você pode modificar alguns dos valores usados para as tarefas definindo-os para atributos Property nos objetos Variant. Como os valores Property só serão resolvidos quando as tarefas do AGP forem executadas, você poderá conectá-las com segurança a provedores de tarefas personalizadas que realizarão qualquer cálculo necessário, incluindo a leitura em entradas externas, como arquivos ou a rede.

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

Fornecer origens geradas para o build

Seu plug-in pode fornecer alguns tipos de origens geradas, como:

Para consultar a lista completa de origens que podem ser adicionadas, acesse a página da API Sources.

Este snippet de código mostra como adicionar ao conjunto de origem Java uma pasta personalizada chamada ${variant.name} usando a função addStaticSourceDirectory(). O conjunto de ferramentas do Android processa essa pasta.

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

Consulte o roteiro addJavaSource (link em inglês) para mais detalhes.

Este snippet de código mostra como adicionar um diretório com recursos do Android gerados por uma tarefa personalizada para o conjunto de origem res. O processo é semelhante para outros tipos de origem.

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

Consulte o roteiro addCustomAsset (link em inglês) para mais detalhes.

Acessar e modificar artefatos

Além de permitir a modificação de propriedades simples nos objetos Variant, o AGP também contém um mecanismo de extensão que permite ler ou transformar artefatos intermediários e finais produzidos durante o build. Por exemplo, você pode ler o conteúdo final do arquivo AndroidManifest.xml mesclado em uma Task personalizada para analisá-lo ou substituir todo o conteúdo pelo de um arquivo de manifesto gerado por sua Task personalizada.

Veja a lista de artefatos atualmente compatíveis na documentação de referência da classe Artifact. Cada tipo de artefato tem determinadas propriedades úteis:

Cardinalidade

A cardinalidade de um Artifact representa o número de instâncias de FileSystemLocation ou o número de arquivos ou diretórios do tipo de artefato. Para ver informações sobre a cardinalidade de um artefato, verifique a classe pai: artefatos com uma única FileSystemLocation serão uma subclasse de Artifact.Single. Os artefatos com várias instâncias de FileSystemLocation serão uma subclasse de Artifact.Multiple.

Tipo de FileSystemLocation

Você pode verificar se um Artifact representa arquivos ou diretórios analisando o tipo de FileSystemLocation parametrizado, que pode ser um RegularFile ou um Directory (links em inglês):

Operações compatíveis

Cada classe Artifact pode implementar as seguintes interfaces para indicar quais operações são compatíveis:

  • Transformable: permite que um Artifact seja usado como entrada para uma Task que executa transformações arbitrárias nele e gera uma nova versão do Artifact.
  • Appendable: aplica-se apenas a artefatos que são subclasses de Artifact.Multiple. Isso significa que o Artifact pode ser anexado, ou seja, uma Task personalizada pode criar novas instâncias desse tipo de Artifact que serão adicionadas à lista existente.
  • Replaceable: aplica-se apenas a artefatos que são subclasses de Artifact.Single. Um Artifact substituível pode ser substituído por uma instância totalmente nova, produzida como saída de uma Task.

Além das três operações de modificação de artefatos, cada artefato é compatível com uma operação get() (ou getAll()), que retorna um Provider com a versão final do artefato, após a conclusão de todas as operações.

Vários plug-ins podem adicionar qualquer número de operações em artefatos ao pipeline pelo callback onVariants(), e o AGP garantirá que eles sejam encadeados corretamente para que todas as tarefas sejam executadas no momento certo e os artefatos sejam produzidos e atualizados. Isso significa que, quando uma operação mudar alguma saída, anexando-a, substituindo-a ou transformando-a, a próxima operação verá a versão atualizada desses artefatos como entradas e assim por diante.

O ponto de entrada para o registro de operações é a classe Artifacts. O snippet de código abaixo mostra como conseguir acesso a uma instância de Artifacts em uma propriedade no objeto Variant no callback onVariants().

Você pode transmitir seu TaskProvider personalizado para receber um objeto TaskBasedOperation (1) e usá-lo para conectar as entradas e saídas usando um dos métodos wiredWith* (2).

O método exato que você precisa escolher depende da cardinalidade e do tipo de FileSystemLocation implementados pelo Artifact que você quer transformar.

Por fim, você transmite no tipo de Artifact para um método que representa a operação escolhida no objeto *OperationRequest que você recebe de volta, por exemplo, 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)
}

Nesse exemplo, MERGED_MANIFEST é um SingleArtifact e é um RegularFile. Por isso, precisamos usar o método wiredWithFiles, que aceita uma única referência de RegularFileProperty para a entrada e uma única RegularFileProperty para a saída. Existem outros métodos wiredWith* na classe TaskBasedOperation que funcionarão para outras combinações de cardinalidade de Artifact e tipos de FileSystemLocation.

Para saber mais sobre a extensão do AGP, recomendamos a leitura das seguintes seções do manual do sistema de build do Gradle (links em inglês):