Cómo extender el complemento de Android para Gradle

El complemento de Android para Gradle (AGP) es el sistema de compilación compatible con aplicaciones para Android. Además, admite la compilación de muchos tipos diferentes de fuentes y su vinculación a una aplicación que puedes ejecutar en un dispositivo Android físico o en un emulador.

AGP contiene puntos de extensión para que los complementos controlen las entradas de compilación y extiendan su funcionalidad mediante pasos nuevos que se pueden integrar con tareas de compilación estándar. Las versiones anteriores del AGP no tenían API oficiales separadas claramente de implementaciones internas. A partir de la versión 7.0, AGP tiene un conjunto de API oficiales y estables en las que puedes confiar.

Ciclo de vida de las API de AGP

AGP sigue el ciclo de vida de las funciones de Gradle para designar el estado de sus API:

  • Interna: No está diseñada para uso público.
  • En preparación: Una versión que no es la final, pero que se encuentra disponible para uso público, lo que significa que puede que no tenga retrocompatibilidad en la versión final.
  • Pública: Disponible para uso público y estable.
  • Obsoleta: Ya no es compatible y se reemplazó con API nuevas.

Política de baja

AGP está avanzando con la baja de las API anteriores y su reemplazo con API nuevas y estables, además de un nuevo lenguaje específico de dominio (DSL). Esta evolución abarcará varias versiones del AGP. Obtén más información al respecto en el cronograma de migración del DSL y la API de AGP.

Cuando las API de AGP dejen de estar disponibles para esta migración o en otros casos, seguirán estando disponibles en la versión principal actual, pero generarán advertencias. Las API obsoletas se quitarán por completo de AGP en la versión principal posterior. Por ejemplo, si una API deja de estar disponible en AGP 7.0, estará disponible en esa versión y generará advertencias. Esa API ya no estará disponible en AGP 8.0.

Para ver ejemplos de API nuevas que se usan en personalizaciones de compilación comunes, consulta las recetas de complemento de Android para Gradle. Allí, se proporcionan ejemplos de personalizaciones de compilación comunes. También puedes obtener más detalles sobre las API nuevas en nuestra documentación de referencia.

Conceptos básicos de compilación de Gradle

En esta guía, no se abarca todo el sistema de compilación de Gradle. Sin embargo, sí se incluye el conjunto mínimo de conceptos necesarios para ayudarte a integrar nuestras API, además de vínculos a la documentación principal de Gradle para consultas adicionales.

Ten en cuenta que se requieren conocimientos básicos sobre el funcionamiento de Gradle, como configuración de proyectos, edición archivos de compilación, uso de complementos y ejecución de tareas. Para conocer los conceptos básicos de Gradle en relación con el AGP, te recomendamos que revises el artículo para configurar tu compilación. Si quieres obtener más información sobre el framework general para personalizar complementos de Gradle, consulta Developing Custom Gradle Plugins (Cómo desarrollar complementos de Gradle personalizados).

Glosario de tipos diferidos de Gradle

Gradle ofrece varios tipos que se comportan "de manera diferida" o ayudan a diferir los cálculos pesados o la creación de Task a fases posteriores de la compilación. Estos tipos son centrales en muchas API de Gradle y AGP. En la siguiente lista se incluyen los tipos principales de Gradle que se usan en la ejecución diferida y sus métodos clave.

Provider<T>
Proporciona un valor de tipo T (donde "T" significa cualquier tipo), que se puede leer durante la fase de ejecución con get() o se transformará en un Provider<S> nuevo (donde "S" significa algún otro tipo) que utiliza los métodos map(), flatMap() y zip(). Ten en cuenta que nunca se debe llamar a get() durante la fase de configuración.
  • map(): Acepta una lambda y produce un Provider de tipo S, Provider<S>. El argumento lambda para map() toma el valor T y produce el valor S. La lambda no se ejecuta de inmediato. En cambio, su ejecución se difiere al momento en que se llama a get() en el Provider<S> resultante, lo que hace que toda la cadena sea diferida.
  • flatMap(): También acepta una lambda y produce Provider<S>, pero la lambda toma un valor T y produce Provider<S> (en lugar de producir el valor S directamente). Usa flatMap() cuando no se pueda determinar S en el momento de la configuración y solo puedas obtener Provider<S>. En términos prácticos, si usaste map() y obtuviste un tipo de resultado Provider<Provider<S>>, es probable que debas usar flatMap() en su lugar.
  • zip(): Te permite combinar dos instancias de Provider para producir un Provider nuevo, con un valor calculado mediante una función que combina los valores de las dos instancias de Providers de entrada.
Property<T>
Implementa Provider<T>, por lo que también brinda un valor de tipo T. A diferencia de lo que ocurre con Provider<T>, que es de solo lectura, también puedes establecer un valor para la Property<T>. Puedes hacerlo de la siguientes dos maneras:
  • Establecer un valor de tipo T directamente cuando esté disponible, sin la necesidad de realizar cálculos diferidos
  • Configurar otro Provider<T> como la fuente del valor de la Property<T> (en este caso, el valor T se materializa solo cuando se llama a Property.get())
TaskProvider
Implementa Provider<Task>. Para generar un TaskProvider, usa tasks.register() y no tasks.create() a fin de asegurarte de que solo se creen instancias de manera diferida cuando sean necesarias. Puedes usar flatMap() para acceder a los resultados de un Task antes de que se cree Task, lo que puede ser útil si deseas usar las salidas como entradas para otras instancias de Task.

Los proveedores y sus métodos de transformación son esenciales para configurar entradas y salidas de tareas de manera diferida, es decir, sin necesidad de crear todas las tareas por adelantado y resolver los valores.

Los proveedores también tienen información sobre la dependencia de las tareas. Cuando creas un Provider mediante la transformación de un resultado de Task, ese Task se convierte en una dependencia implícita de Provider y se creará y ejecutará cada vez que se resuelva el valor de Provider, como en caso de que otro Task lo requiera.

A continuación, se muestra un ejemplo para registrar dos tareas, GitVersionTask y ManifestProducerTask, y diferir la creación de las instancias de Task hasta que sean obligatorias. El valor de entrada de ManifestProducerTask se establece en un Provider que se obtuvo del resultado de GitVersionTask, por lo que ManifestProducerTask depende de manera implícita 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))
    }

Estas dos tareas solo se ejecutarán si se solicitan de explícitamente. Esto puede suceder como parte de una invocación de Gradle, por ejemplo, si ejecutas ./gradlew debugManifestProducer o si el resultado de ManifestProducerTask está conectado a alguna otra tarea y se requiere su valor.

Aunque en el futuro escribas tareas personalizadas que consuman entradas o produzcan resultados, AGP no ofrece acceso público a sus propias tareas directamente. Son un detalle de implementación sujeto a cambios de versión a versión. En su lugar, AGP ofrece la API de variantes y acceso al resultado de sus tareas (o artefactos de compilación) que puedes leer y transformar. Para obtener más información, consulta API de variantes, artefactos y tareas en este documento.

Fases de la compilación de Gradle

La compilación de un proyecto es inherentemente un proceso complicado que requiere muchos recursos. Sin embargo, hay varias funciones, como la elusión de la configuración de tareas, las verificaciones actualizadas y la función de almacenamiento en caché de configuración, que ayudan a minimizar el tiempo invertido en cálculos reproducibles o innecesarios.

Para aplicar algunas de estas optimizaciones, las secuencias de comandos y los complementos de Gradle deben cumplir con reglas estrictas durante cada una de las distintas fases de compilación de Gradle: inicialización, configuración y ejecución. En esta guía, nos enfocaremos en las fases de configuración y ejecución. Puedes encontrar más información sobre todas las fases en la guía del ciclo de vida de compilación de Gradle.

Fase de configuración

Durante la fase de configuración, se evalúan las secuencias de comandos de compilación de todos los proyectos que forman parte de la compilación, se aplican los complementos y se resuelven las dependencias de compilación. Esta fase debe usarse para configurar la compilación mediante objetos DSL y para registrar tareas y sus entradas de manera diferida.

Debido a que la fase de configuración se ejecuta siempre, independientemente de la tarea que se solicita para ejecución, es muy importante que sea eficiente y evitar que los cálculos dependan de entradas que no sean las secuencias de comandos de compilación. Es decir, no debes ejecutar programas externos ni leer desde la red ni realizar cálculos largos que se puedan diferir a la fase de ejecución como instancias de Task adecuadas.

Fase de ejecución

En la fase de ejecución, se ejecutan las tareas solicitadas y las tareas dependientes. En concreto, se ejecutan los métodos de clase de Task marcados con @TaskAction. Durante la ejecución de las tareas, si llamas a Provider<T>.get(), puedes leer las entradas (como los archivos) y resolver los proveedores diferidos. Cuando se resuelven los proveedores diferidos, se inicia una secuencia de llamadas map() o flatMap() que siguen la información de dependencia de las tareas incluida en el proveedor. Las tareas se ejecutan de manera diferida para materializar los valores requeridos.

API de variantes, artefactos y tareas

La API de variantes es un mecanismo de extensión en el complemento de Android para Gradle que te permite manipular las distintas opciones, que suelen establecerse con DSL en los archivos de configuración de compilación que influyen en la compilación de Android. La API de variantes también te brinda acceso a artefactos intermedios y finales que crea la compilación, como los archivos de clase, el manifiesto combinado o los archivos APK/AAB.

Flujo de compilación y puntos de extensión de Android

Cuando interactúas con AGP, usa sus puntos de extensión creados en lugar de registrar las devoluciones típicas de llamada de ciclo de vida de Gradle (como afterEvaluate()) o configurar dependencias explícitas de Task. Las tareas creadas por AGP se consideran detalles de la implementación y no se exponen como una API pública. Evita obtener instancias de los objetos Task o adivinar los nombres de Task y agregar devoluciones de llamada o dependencias a esos objetos de Task directamente.

AGP completa los siguientes pasos para crear y ejecutar sus instancias de Task, que, a su vez, producen los artefactos de compilación. Después de los pasos principales que se incluyen en la creación de objetos Variant, siguen las devoluciones de llamada que te permiten realizar cambios en ciertos objetos creados como parte de una compilación. Es importante tener en cuenta que todas las devoluciones de llamada ocurren durante la fase de configuración (que se describe en este documento) y que deben ejecutarse rápido, de forma tal que se difiera cualquier trabajo complicado en las instancias de Task adecuadas durante la fase de ejecución.

  1. Análisis de DSL: Aquí se evalúan las secuencias de comandos de compilación y se crean y configuran las distintas propiedades de los objetos DSL de Android del bloque android. Las devoluciones de llamada de la API de variantes que se describen en las siguientes secciones también se registran durante esta fase.
  2. finalizeDsl(): Es la devolución de llamada que te permite cambiar objetos DSL antes de que se bloqueen para la creación de componentes (variantes). Los objetos VariantBuilder se crean a partir de datos contenidos en los objetos DSL.

  3. Bloqueo de DSL: DSL está bloqueado y ya no es posible hacer cambios.

  4. beforeVariants(): Esta devolución de llamada puede influir en los componentes que se crean y en algunas de sus propiedades, por medio de VariantBuilder. Aun así, permite modificaciones en el flujo de compilación y en los artefactos que se producen.

  5. Creación de variantes: Se completó la lista de componentes y artefactos que se crearán y no se puede cambiar.

  6. onVariants(): En esta devolución de llamada, obtendrás acceso a los objetos Variant creados y puedes establecer valores o proveedores de los valores de Property que contienen para que se calculen de manera diferida.

  7. Bloqueo de variantes: Los objetos de variantes ahora están bloqueados y ya no es posible realizar cambios.

  8. Tareas creadas: Los objetos Variant y sus valores Property se usan para crear las instancias de Task que son necesarias a fin de ejecutar la compilación.

AGP introduce una AndroidComponentsExtension que te permite registrar devoluciones de llamada para finalizeDsl(), beforeVariants() y onVariants(). La extensión está disponible en las secuencias de comandos de compilación por medio del bloque androidComponents:

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

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

Sin embargo, recomendamos mantener las secuencias de comandos de compilación solo para la configuración declarativa usando el DSL del bloque de Android y mover la lógica imperativa personalizada a buildSrc o a complementos externos. También puedes consultar las muestras de buildSrc en el repositorio de GitHub de recetas de Gradle para aprender a crear un complemento en tu proyecto. A continuación, se muestra un ejemplo para registrar devoluciones de llamada desde el código del complemento:

abstract class ExamplePlugin: Plugin<Project> {

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

Analicemos con más detalle las devoluciones de llamada disponibles y el tipo de casos de uso que tu complemento puede admitir en cada una de ellas:

finalizeDsl(callback: (DslExtensionT) -> Unit)

En esta devolución de llamada, puedes acceder a los objetos DSL que se crearon y modificarlos mediante el análisis de la información del bloque android en los archivos de compilación. Estos objetos DSL se usarán para inicializar y configurar variantes en las fases posteriores de la compilación. Por ejemplo, puedes crear configuraciones nuevas de manera programática o anular propiedades. Sin embargo, ten en cuenta que todos los valores deben resolverse en el momento de la configuración, por lo que no deben depender de las entradas externas. Una vez que se termina de ejecutar esta devolución de llamada, los objetos DSL ya no son útiles y ya no debes tener referencias a ellos ni modificar sus 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()

En esta etapa de la compilación, obtendrás acceso a objetos VariantBuilder, que determinan las variantes que se crearán y sus propiedades. Por ejemplo, puedes inhabilitar de manera programática determinadas variantes y sus pruebas o cambiar el valor de una propiedad (por ejemplo, minSdk) solo para una variante seleccionada. De manera similar a finalizeDsl(), todos los valores que proporcionas deben resolverse en el momento de la configuración y no depender de entradas externas. Los objetos VariantBuilder no deben modificarse una vez que termina la ejecución de la devolución de llamada de beforeVariants().

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

La devolución de llamada de beforeVariants(), de forma opcional, toma un VariantSelector, que puedes obtener por medio del método de selector() en la androidComponentsExtension. Puedes usarla para filtrar los componentes que participan en la invocación de devolución de llamada en función del nombre, el tipo de compilación o la variante de producto.

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

onVariants()

Antes de llamar a onVariants(), ya se decidieron todos los artefactos que creará AGP, por lo que ya no puedes inhabilitarlos. Sin embargo, puedes modificar algunos de los valores que se usan para las tareas si los configuras en los atributos Property en los objetos Variant. Debido a que los valores Property solo se resolverán cuando se ejecuten las tareas del AGP, puedes vincularlos de forma segura con los proveedores de tus propias tareas personalizadas que realizarán cualquier cálculo requerido, incluida la lectura desde entradas externas como archivos o la red.

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

Acceso y modificación de artefactos

Además de permitirte modificar propiedades simples en los objetos Variant, AGP también contiene un mecanismo de extensión que te permite leer o transformar artefactos intermedios y finales que se produjeron durante la compilación. Por ejemplo, puedes leer el contenido combinado final del archivo AndroidManifest.xml en una Task personalizada para analizarlo o puedes reemplazar todo su contenido por el de un archivo de manifiesto generado por tu Task personalizada.

Puedes encontrar la lista de artefactos que se admiten actualmente en la documentación de referencia de la clase Artifact. Cada tipo de artefacto tiene propiedades determinadas que son útiles para conocer lo siguiente:

Cardinalidad

La cardinalidad de un Artifact representa la cantidad de instancias de FileSystemLocation o la cantidad de archivos o directorios del tipo de artefacto. Puedes obtener información sobre la cardinalidad de un artefacto si verificas su clase superior. Los artefactos con una sola FileSystemLocation serán una subclase de Artifact.Single; los artefactos con varias instancias de FileSystemLocation serán una subclase de Artifact.Multiple.

Tipo de FileSystemLocation

Para verificar si un Artifact representa archivos o directorios, consulta el tipo de FileSystemLocation parametrizada, que puede ser un RegularFile o un Directory.

Operaciones admitidas

Cada clase de Artifact puede implementar cualquiera de las siguientes interfaces para indicar el tipo de operaciones que admite:

  • Transformable: Permite usar un objeto Artifact como entrada para un objeto Task que realiza transformaciones arbitrarias en él y genera una versión nueva del objeto Artifact.
  • Appendable: Se aplica solo a los artefactos que son subclases de Artifact.Multiple. Significa que Artifact se puede agregar, es decir, una Task personalizada puede crear instancias nuevas de este tipo de Artifact que se agregarán a la lista existente.
  • Replaceable: Se aplica solo a los artefactos que son subclases de Artifact.Single. Un Artifact reemplazable se puede reemplazar por una instancia completamente nueva, producida como resultado de una Task.

Además de las tres operaciones de modificación de artefactos, cada artefacto admite una operación get() (o getAll()), que muestra un Provider con la versión final del artefacto (después de que se completan todas las operaciones).

Varios complementos pueden agregar cualquier cantidad de operaciones en artefactos a la canalización desde la devolución de llamada onVariants(). Además, AGP garantizará que se encadenen adecuadamente para que todas las tareas se ejecuten en el momento adecuado y los artefactos estén actualizados y se produzcan de forma correcta. Esto significa que, cuando una operación cambia los resultados mediante anexos, reemplazos o transformaciones, la próxima operación verá la versión actualizada de estos artefactos como entradas, y así sucesivamente.

El punto de entrada para registrar operaciones es la clase Artifacts. En el siguiente fragmento de código, se muestra el modo en el que puedes obtener acceso a una instancia de Artifacts desde una propiedad en el objeto Variant de la devolución de llamada onVariants().

Luego, puedes pasar tu TaskProvider personalizado para obtener un objeto TaskBasedOperation (1) y usarlo a fin de conectar sus entradas y salidas con uno de los métodos wiredWith* (2).

El método exacto que debes elegir depende de la cardinalidad y el tipo de FileSystemLocation implementado por el Artifact que deseas transformar.

Y, por último, pasa el tipo de Artifact a un método que represente la operación elegida en el objeto *OperationRequest que obtienes como resultado, por ejemplo, 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)
}

En este ejemplo, MERGED_MANIFEST es un SingleArtifact y es un RegularFile. Debido a eso, debemos usar el método wiredWithFiles, que acepta una sola referencia de RegularFileProperty para la entrada y una sola RegularFileProperty para la salida. Existen otros métodos de wiredWith* en la clase TaskBasedOperation que funcionarán para otras combinaciones de cardinalidad de Artifact y tipos de FileSystemLocation.

Para obtener más información a fin de extender el AGP, te recomendamos que leas las siguientes secciones del manual del sistema de compilación de Gradle: