Написание плагинов Gradle

Плагин Android Gradle (AGP) — официальная система сборки приложений Android. Он включает в себя поддержку компиляции множества различных типов источников и объединения их в приложение, которое можно запустить на физическом устройстве Android или в эмуляторе.

AGP содержит точки расширения для плагинов для управления входными данными сборки и расширения ее функциональности за счет новых шагов, которые можно интегрировать со стандартными задачами сборки. Предыдущие версии AGP не имели официальных API, четко отделенных от внутренних реализаций. Начиная с версии 7.0, AGP имеет набор официальных стабильных API , на которые вы можете положиться.

Жизненный цикл API AGP

AGP следует жизненному циклу функций Gradle , чтобы обозначить состояние своих API:

  • Внутренний : Не предназначен для публичного использования.
  • Инкубация : доступна для публичного использования, но не является окончательной версией. Это означает, что в финальной версии они могут не иметь обратной совместимости.
  • Публичный : доступен для публичного использования и стабильный.
  • Устарело : больше не поддерживается и заменено новыми API.

Политика прекращения поддержки

AGP развивается по мере устаревания старых API и их замены новыми, стабильными API и новым доменно-специфическим языком (DSL). Эта эволюция будет охватывать несколько выпусков AGP, и вы можете узнать больше об этом на временной шкале миграции AGP API/DSL .

Когда API-интерфейсы AGP устаревают, в связи с этой миграцией или иным образом, они будут по-прежнему доступны в текущей основной версии, но будут генерировать предупреждения. Устаревшие API будут полностью удалены из AGP в следующем основном выпуске. Например, если API устарел в AGP 7.0, он будет доступен в этой версии и будет генерировать предупреждения. Этот API больше не будет доступен в AGP 8.0.

Чтобы увидеть примеры новых API, используемых в общих настройках сборки, ознакомьтесь с рецептами плагинов Android Gradle . Они предоставляют примеры общих настроек сборки. Более подробную информацию о новых API вы также можете найти в нашей справочной документации .

Основы сборки Gradle

Это руководство не охватывает всю систему сборки Gradle. Тем не менее, он охватывает минимально необходимый набор концепций, которые помогут вам интегрироваться с нашими API, и содержит ссылки на основную документацию Gradle для дальнейшего чтения.

Мы предполагаем базовые знания о том, как работает Gradle, в том числе о том, как настраивать проекты, редактировать файлы сборки, применять плагины и запускать задачи. Чтобы узнать об основах Gradle применительно к AGP, мы рекомендуем просмотреть статью Настройка сборки . Чтобы узнать об общей структуре настройки плагинов Gradle, см. «Разработка пользовательских плагинов Gradle» .

Глоссарий ленивых типов Gradle

Gradle предлагает ряд типов, которые ведут себя «лениво» или помогают отложить тяжелые вычисления или создание Task на более поздние этапы сборки. Эти типы лежат в основе многих API Gradle и AGP. В следующем списке представлены основные типы Gradle, участвующие в отложенном выполнении, и их ключевые методы.

Provider<T>
Предоставляет значение типа T (где «T» означает любой тип), которое можно прочитать на этапе выполнения с помощью get() или преобразовать в новый Provider<S> (где «S» означает какой-либо другой тип) с помощью map() . map() , flatMap() и zip() . Обратите внимание, что get() никогда не следует вызывать на этапе настройки.
  • map() : принимает лямбду и создает Provider типа S , Provider<S> . Аргумент лямбда функции map() принимает значение T и выдает значение S Лямбда-выражение не выполняется немедленно; вместо этого его выполнение откладывается до момента вызова get() для результирующего Provider<S> , что делает всю цепочку ленивой.
  • flatMap() : также принимает лямбду и создает Provider<S> , но лямбда принимает значение T и создает Provider<S> (вместо того, чтобы напрямую создавать значение S ). Используйте FlatMap(), когда S не может быть определен во время настройки и вы можете получить только Provider<S> . Фактически, если вы использовали map() и получили тип результата Provider<Provider<S>> , это, вероятно, означает, что вместо этого вам следовало использовать flatMap() .
  • zip() : позволяет объединить два экземпляра Provider для создания нового Provider со значением, вычисляемым с помощью функции, которая объединяет значения из двух входных экземпляров Providers .
Property<T>
Реализует Provider<T> , поэтому он также предоставляет значение типа T . В отличие от Provider<T> , который доступен только для чтения, вы также можете установить значение для Property<T> . Есть два способа сделать это:
  • Установите значение типа T напрямую, когда оно доступно, без необходимости отложенных вычислений.
  • Установите другой Provider<T> в качестве источника значения Property<T> . В этом случае значение T материализуется только при вызове Property.get() .
TaskProvider
Реализует Provider<Task> . Чтобы сгенерировать TaskProvider , используйте tasks.register() , а не tasks.create() , чтобы обеспечить ленивое создание экземпляров задач только тогда, когда они необходимы. Вы можете использовать flatMap() для доступа к выходным данным Task до Task создания, что может быть полезно, если вы хотите использовать выходные данные в качестве входных данных для других экземпляров Task .

Провайдеры и их методы преобразования необходимы для настройки входных и выходных данных задач ленивым способом, то есть без необходимости заранее создавать все задачи и разрешать значения.

Поставщики также передают информацию о зависимостях задач. Когда вы создаете Provider путем преобразования выходных данных Task , эта Task становится неявной зависимостью Provider и будет создаваться и запускаться всякий раз, когда значение Provider будет разрешено, например, когда этого требует другая Task .

Вот пример регистрации двух задач, GitVersionTask и ManifestProducerTask , с отложенным созданием экземпляров Task до тех пор, пока они действительно не потребуются. Входное значение ManifestProducerTask установлено на Provider полученный из выходных данных GitVersionTask , поэтому ManifestProducerTask неявно зависит от 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))
    }

Эти две задачи будут выполняться только в том случае, если они явно запрошены. Это может произойти как часть вызова Gradle, например, если вы запустите ./gradlew debugManifestProducer или если выходные данные ManifestProducerTask подключены к какой-либо другой задаче и его значение становится обязательным.

Хотя вы будете писать собственные задачи, которые потребляют входные данные и/или производят выходные данные, AGP не предлагает публичный доступ напрямую к своим собственным задачам. Это детали реализации, которые могут меняться от версии к версии. Вместо этого AGP предлагает Variant API и доступ к результатам выполнения своих задач или созданию артефактов , которые вы можете читать и преобразовывать. Дополнительные сведения см. в разделе Варианты API, артефакты и задачи в этом документе.

Этапы сборки Gradle

Создание проекта по своей сути является сложным и ресурсоемким процессом, и существуют различные функции, такие как предотвращение конфигурации задач, проверка актуальности и функция кэширования конфигурации, которые помогают минимизировать время, затрачиваемое на воспроизводимые или ненужные вычисления.

Чтобы применить некоторые из этих оптимизаций, сценарии и плагины Gradle должны подчиняться строгим правилам на каждом из отдельных этапов сборки Gradle: инициализации, настройки и выполнения. В этом руководстве мы сосредоточимся на этапах настройки и выполнения. Более подробную информацию обо всех этапах вы можете найти в руководстве по жизненному циклу сборки Gradle .

Этап настройки

На этапе настройки оцениваются сценарии сборки для всех проектов, входящих в сборку, применяются плагины и разрешаются зависимости сборки. Этот этап следует использовать для настройки сборки с использованием объектов DSL и для ленивой регистрации задач и их входных данных.

Поскольку фаза настройки выполняется всегда, независимо от того, какую задачу требуется выполнить, особенно важно сохранить ее компактность и ограничить зависимость любых вычислений от входных данных, отличных от самих сценариев сборки. То есть вам не следует выполнять внешние программы или читать из сети, а также выполнять длительные вычисления, которые можно отложить до фазы выполнения в качестве соответствующих экземпляров Task .

Этап исполнения

На этапе выполнения выполняются запрошенные задачи и зависимые от них задачи. В частности, выполняются методы класса Task , отмеченные @TaskAction . Во время выполнения задачи вам разрешено читать входные данные (например, файлы) и разрешать ленивых поставщиков, вызывая Provider<T>.get() . Разрешение ленивых поставщиков таким способом запускает последовательность вызовов map() или flatMap() , которые следуют за информацией о зависимостях задач, содержащейся в поставщике. Задачи выполняются лениво, чтобы материализовать необходимые значения.

Вариант API, артефакты и задачи

Variant API — это механизм расширения в плагине Android Gradle, который позволяет вам манипулировать различными параметрами, обычно устанавливаемыми с помощью DSL в файлах конфигурации сборки, которые влияют на сборку Android. Variant API также предоставляет вам доступ к промежуточным и конечным артефактам, созданным в ходе сборки, таким как файлы классов, объединенный манифест или файлы APK/AAB.

Процесс сборки Android и точки расширения

При взаимодействии с AGP используйте специально созданные точки расширения вместо регистрации типичных обратных вызовов жизненного цикла Gradle (таких как afterEvaluate() ) или настройки явных зависимостей Task . Задачи, созданные AGP, считаются деталями реализации и не предоставляются как общедоступный API. Вам следует избегать попыток получить экземпляры объектов Task или угадывать имена Task и напрямую добавлять обратные вызовы или зависимости к этим объектам Task .

AGP выполняет следующие шаги для создания и выполнения своих экземпляров Task , которые, в свою очередь, создают артефакты сборки. За основными этапами создания объекта Variant следуют обратные вызовы, которые позволяют вносить изменения в определенные объекты, созданные в рамках сборки. Важно отметить, что все обратные вызовы происходят на этапе настройки (описанном на этой странице) и должны выполняться быстро, вместо этого откладывая любую сложную работу на соответствующие экземпляры Task на этапе выполнения.

  1. Анализ DSL : при этом оцениваются сценарии сборки, а также создаются и устанавливаются различные свойства объектов Android DSL из блока android . На этом этапе также регистрируются обратные вызовы Variant API, описанные в следующих разделах.
  2. finalizeDsl() : обратный вызов, позволяющий изменять объекты DSL до того, как они будут заблокированы для создания компонента (варианта). Объекты VariantBuilder создаются на основе данных, содержащихся в объектах DSL.

  3. Блокировка DSL : DSL теперь заблокирован, и изменения больше невозможны.

  4. beforeVariants() : этот обратный вызов может влиять на то, какие компоненты и некоторые их свойства создаются с помощью VariantBuilder . Он по-прежнему позволяет вносить изменения в процесс сборки и создаваемые артефакты.

  5. Создание варианта : список компонентов и артефактов, которые будут созданы, завершен и не может быть изменен.

  6. onVariants() : в этом обратном вызове вы получаете доступ к созданным объектам Variant и можете установить значения или поставщиков для значений Property , которые они содержат, для ленивого вычисления.

  7. Блокировка вариантов : объекты вариантов теперь заблокированы, и изменения больше невозможны.

  8. Созданные задачи : объекты Variant и значения их Property используются для создания экземпляров Task , необходимых для выполнения сборки.

AGP представляет AndroidComponentsExtension , который позволяет регистрировать обратные вызовы для finalizeDsl() , beforeVariants() и onVariants() . Расширение доступно в скриптах сборки через блок androidComponents :

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

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

Однако мы рекомендуем сохранять сценарии сборки только для декларативной конфигурации с использованием DSL блока Android и перемещать любую пользовательскую императивную логику в buildSrc или внешние плагины. Вы также можете просмотреть примеры buildSrc в нашем репозитории рецептов Gradle на GitHub, чтобы узнать, как создать плагин в вашем проекте. Вот пример регистрации обратных вызовов из кода плагина:

abstract class ExamplePlugin: Plugin<Project> {

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

Давайте подробнее рассмотрим доступные обратные вызовы и типы вариантов использования, которые ваш плагин может поддерживать в каждом из них:

finalizeDsl(callback: (DslExtensionT) -> Unit)

В этом обратном вызове вы можете получить доступ к объектам DSL, созданным путем анализа информации из блока android в файлах сборки, и изменить их. Эти объекты DSL будут использоваться для инициализации и настройки вариантов на более поздних этапах сборки. Например, вы можете программно создавать новые конфигурации или переопределять свойства, но имейте в виду, что все значения должны быть определены во время настройки, поэтому они не должны зависеть от каких-либо внешних входных данных. После завершения выполнения этого обратного вызова объекты DSL перестают быть полезными, и вам больше не следует хранить ссылки на них или изменять их значения.

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

На этом этапе сборки вы получаете доступ к объектам VariantBuilder , которые определяют создаваемые варианты и их свойства. Например, вы можете программно отключить определенные варианты, их тесты или изменить значение свойства (например, minSdk ) только для выбранного варианта. Как и в случае с finalizeDsl() , все предоставленные вами значения должны быть разрешены во время настройки и не зависеть от внешних входных данных. Объекты VariantBuilder не должны изменяться после завершения выполнения обратного вызова beforeVariants() .

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

Обратный вызов beforeVariants() дополнительно принимает VariantSelector , который вы можете получить с помощью метода selector() в androidComponentsExtension . Вы можете использовать его для фильтрации компонентов, участвующих в вызове обратного вызова, на основе их имени, типа сборки или разновидности продукта.

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

onVariants()

К моменту вызова onVariants() все артефакты, которые будут созданы AGP, уже определены, поэтому вы больше не можете их отключить. Однако вы можете изменить некоторые значения, используемые для задач, установив их для атрибутов Property в объектах Variant . Поскольку значения Property будут определены только при выполнении задач AGP, вы можете безопасно подключить их к поставщикам из ваших собственных пользовательских задач, которые будут выполнять любые необходимые вычисления, включая чтение из внешних входных данных, таких как файлы или сеть.

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

Добавьте сгенерированные исходные коды в сборку.

Ваш плагин может предоставлять несколько типов сгенерированных источников, таких как:

Полный список источников, которые вы можете добавить, см. в разделе Sources API .

В этом фрагменте кода показано, как добавить пользовательскую папку исходного кода с именем ${variant.name} в набор исходного кода Java с помощью функции addStaticSourceDirectory() . Затем цепочка инструментов Android обрабатывает эту папку.

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

Более подробную информацию смотрите в рецепте addJavaSource .

В этом фрагменте кода показано, как добавить каталог с ресурсами Android, созданными из пользовательской задачи, в набор источников res . Процесс аналогичен для других типов источников.

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

Дополнительные сведения см. в рецепте addCustomAsset .

Доступ и изменение артефактов

Помимо возможности изменять простые свойства объектов Variant , AGP также содержит механизм расширения, который позволяет вам читать или преобразовывать промежуточные и конечные артефакты, созданные во время сборки. Например, вы можете прочитать окончательное объединенное содержимое файла AndroidManifest.xml в пользовательской Task для его анализа или полностью заменить его содержимое содержимым файла манифеста, созданного вашей пользовательской Task .

Список поддерживаемых в настоящее время артефактов можно найти в справочной документации по классу Artifact . Каждый тип артефакта имеет определенные свойства, которые полезно знать:

Мощность

Мощность Artifact представляет собой количество экземпляров FileSystemLocation или количество файлов или каталогов типа артефакта. Вы можете получить информацию о мощности артефакта, проверив его родительский класс: Артефакты с одним FileSystemLocation будут подклассом Artifact.Single ; артефакты с несколькими экземплярами FileSystemLocation будут подклассом Artifact.Multiple .

Тип FileSystemLocation

Вы можете проверить, представляет ли Artifact файлы или каталоги, просмотрев его параметризованный тип FileSystemLocation , который может быть либо RegularFile , либо Directory .

Поддерживаемые операции

Каждый класс Artifact может реализовать любой из следующих интерфейсов, чтобы указать, какие операции он поддерживает:

  • Transformable : позволяет использовать Artifact в качестве входных данных для Task , которая выполняет над ним произвольные преобразования и выводит новую версию Artifact .
  • Appendable : применяется только к артефактам, которые являются подклассами Artifact.Multiple . Это означает, что Artifact может быть добавлен, то есть пользовательская Task может создавать новые экземпляры этого типа Artifact , которые будут добавлены в существующий список.
  • Replaceable : применяется только к артефактам, которые являются подклассами Artifact.Single . Заменяемый Artifact может быть заменен совершенно новым экземпляром, созданным в результате выполнения Task .

В дополнение к трем операциям изменения артефакта, каждый артефакт поддерживает операцию get() (или getAll() ), которая возвращает Provider с окончательной версией артефакта (после завершения всех операций над ним).

Несколько плагинов могут добавлять любое количество операций над артефактами в конвейер из обратного вызова onVariants() , и AGP гарантирует, что они правильно связаны в цепочку, чтобы все задачи выполнялись в нужное время, а артефакты корректно создавались и обновлялись. Это означает, что когда операция изменяет какие-либо выходные данные путем их добавления, замены или преобразования, следующая операция будет видеть обновленную версию этих артефактов в качестве входных данных и так далее.

Точкой входа в регистрацию операций является класс Artifacts . В следующем фрагменте кода показано, как можно получить доступ к экземпляру Artifacts из свойства объекта Variant в обратном вызове onVariants() .

Затем вы можете передать свой пользовательский TaskProvider , чтобы получить объект TaskBasedOperation (1), и использовать его для соединения его входов и выходов с помощью одного из методов wiredWith* (2).

Точный метод, который вам нужно выбрать, зависит от количества элементов и типа FileSystemLocation реализованного Artifact , который вы хотите преобразовать.

И, наконец, вы передаете тип Artifact методу, представляющему выбранную операцию над объектом *OperationRequest , который вы получаете взамен, например, toAppendTo() , toTransform() или 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)
}

В этом примере MERGED_MANIFEST — это SingleArtifact и RegularFile . По этой причине нам необходимо использовать метод wiredWithFiles , который принимает одну ссылку RegularFileProperty на вход и одну RegularFileProperty на выход. В классе TaskBasedOperation есть другие методы wiredWith* , которые будут работать для других комбинаций мощности Artifact и типов FileSystemLocation .

Чтобы узнать больше о расширении AGP, мы рекомендуем прочитать следующие разделы руководства по системе сборки Gradle: