Gradle 与 AGP 构建 API: 如何编写插件

Gradle 与 AGP 构建 API: 如何编写插件

欢迎阅读 MAD Skills 系列 之 Gradle 与 AGP 构建 API 的第二篇文章。通过上篇文章《Gradle 与 AGP 构建 API: 配置您的构建文件》您已经了解 Gradle 的基础知识以及如何配置 Android Gradle Plugin。在本文中,您将学习如何通过编写您自己的插件来扩展您的构建。如果您更喜欢通过视频了解此内容,请在 此处 查看。

Android Gradle Plugin 从 7.0 版开始提供稳定的扩展点,用于操作变体配置和生成的构建产物。该 API 的一些部分是最近才完成的,因此我将会在本文中使用 7.1 版 AGP (撰写本文时尚处于 Beta 版)。

Gradle Task

我会从一个全新的项目开始。如果您想要同步学习,可以通过选择基础 Activity 模板来创建一个新项目。

让我们从创建 Task 并打印输出开始——没错,就是 hello world。为此,我会在应用层的 build.gradle.kts 文件注册一个新的 Task,并将其命名为 "hello"

tasks.register("hello"){ }

现在 Task 已经准备就绪,我们可以打印出 "hello" 并加上项目名称。注意当前 build.gradle.kts 文件属于应用模块,所以 project.name 将会是当前模块的名字 "app"。而如果我是用 project.parent?.name,就会返回项目的名称。

tasks.register("hello"){
   println("Hello " + project.parent?.name)
}

是时候运行该 Task 了。此时查看 Task 列表,可以看到我的 Task 已经位列其中。

△ 新的 Task 已经列在 Android Studio 的 Gradle 窗格中了

△ 新的 Task 已经列在 Android Studio 的 Gradle 窗格中了

我可以双击 hello Task 或通过终端执行此 Task,并在构建输出中观察它所打印的 hello 信息。

△ Task 在构建输出中打印的 hello 信息

△ Task 在构建输出中打印的 hello 信息

在查看日志时,我可以看到此信息是在配置阶段打印的。配置阶段实际上与执行 Task 的功能 (例如本例中的打印 Hello World) 无关。配置阶段是进行 Task 配置以作用于其执行的阶段。您可以在此阶段确定 Task 的输入、参数,以及输出的位置。

无论请求运行哪个 Task,配置阶段都会执行。在配置阶段执行耗时操作会导致较长的配置时间。

Task 的执行应当只在执行阶段发生,所以我们需要将打印调用移动至执行阶段。我可以通过添加 doFirst() 或 doLast() 函数来达到这一目的,二者分别可以在执行阶段的开始和结束时打印 hello 消息。

tasks.register("hello"){
   doLast {
       println("Hello " + project.parent?.name)
   }
}

当我再次运行 Task 时,我可以看到 hello 信息是在执行阶段打印的。

△ 现在 Task 会在执行阶段打印 hello 信息

△ 现在 Task 会在执行阶段打印 hello 信息

我的自定义 Task 目前位于 build.gradle.kts 文件中。添加自定义 Task 到 build.gradle 文件是创建自定义构建脚本的方便法门。不过,在我的插件代码变得愈发复杂时,这种方式不利于进行扩展。我们建议将自定义 Task 和插件实现放置于 buildSrc 文件夹。

在 buildSrc 中实现插件

在编写更多代码前,让我们将 hello Task 移动至 buildSrc。我会创建一个新的文件夹,并将其命名为 buildSrc。接下来,我为插件项目创建了一个 build.gradle.kts 文件,这样 Gradle 就会自动将此文件夹添加至构建。

这是项目根文件夹中的顶层目录。注意,我并不需要在我的项目中将其添加为模块。Gradle 会自动编译目录中的代码,并将其加入到您构建脚本的 classpath 中。

接下来,我创建了一个新的 src 文件夹与一个名为 HelloTask 的类。我将新的类改为 abstract 类,并使其继承 DefaultTask。随后,我会添加一个名为 taskAction 的函数、使用 @TaskAction 注解此函数,并将我自定义的 Task 代码迁移至此函数中。

abstract class HelloTask: DefaultTask() {   
   @TaskAction
   fun taskAction() {
       println("Hello \"${project.parent?.name}\" from task!")
   }
}

现在,我的 Task 已经就绪。我会创建一个新的插件类,这需要实现 Plugin 类型并覆盖 apply() 函数。Gradle 会调用此函数并传入 Project 对象。为了注册 HelloTask,我需要在 project.tasks 上调用 register(),并为这个新的 Task 命名。

class CustomPlugin: Plugin<Project> {
   override fun apply(project: Project) {
       project.tasks.register<HelloTask>("hello")
   }
}

此时,我也可以将我的 Task 声明为依赖其他 Task。

class CustomPlugin: Plugin<Project> {
   override fun apply(project: Project) {
       project.tasks.register<HelloTask>("hello"){
           dependsOn("build")
       }
   }
}

下面让我们应用新的插件。注意,如果我的项目含有多个模块,我也可以通过将此插件加入其他 build.gradle 文件来复用它。

plugins {
   id ("com.android.application")
   id ("org.jetbrains.kotlin.android")
}
apply<CustomPlugin>()
android {
  ...
}

现在,我会运行 hello Task,并像之前一样观察插件的运行。

./gradlew hello

到目前为止,我已经将我的 Task 移至 buildSrc,让我们更进一步,探索新的 Android Gradle Plugin API。AGP 为其构建产物时的生命周期提供了扩展点。

在开始学习 Variant API 前,让我们先了解什么是 Variant。变体 (variant) 是您应用可以构建的不同版本。假设除了功能完整的应用,您还希望构建一个演示版的应用或用于调试的内部版本。您还可以针对不同的目标 API 或设备类型。变体由多个构建类型组合而成,例如 debug 与 release,以及构建脚本中定义的产品变种。

在您的构建文件中,使用声明式 DSL 添加构建类型是完全没有问题的。不过,在代码中以这种方式让您的插件影响构建是不可能的,或者说难以使用声明式语法进行表达。

AGP 通过解析构建脚本及 android 块中设置的属性来启动构建。新的 Variant API 回调让我可以从 androidComponents 扩展中添加 finalizeDSL() 回调。在此回调中,我可以在 DSL 对象应用于 Variant 创建前对它们进行修改。我将创建一个新的构建类型并且设置它的属性。

val extension = project.extensions.getByName(
   "androidComponents"
) as ApplicationAndroidComponentsExtension

extension.finalizeDsl { ext->
   ext.buildTypes.create("staging").let { buildType ->
       buildType.initWith(ext.buildTypes.getByName("debug"))
       buildType.manifestPlaceholders["hostName"] = "example.com"
       buildType.applicationIdSuffix = ".debugStaging"
   }
}

注意,在此阶段中,我可以创建或注册新的构建类型并设置它们的属性。在阶段结束时,AGP 将会锁定 DSL 对象,这样它们就无法再被更改。如果我再次运行构建,我会看到应用的 staging 版本被构建了。

现在,假设我的一个测试没有通过,这时我想要禁用单元测试来构建一个内部版本,以找出问题所在。

为了禁用单元测试,我可以使用 beforeVariants() 回调。该回调可以让我通过 VariantBuilder 对象进行这类修改。在这里,我会检查当前变体是否是我为 staging 创建的变体。接下来,我将禁用单元测试并设置不同的 minSdk 版本。

extension.beforeVariants { variantBuilder ->
   if (variantBuilder.name == "staging") {
       variantBuilder.enableUnitTest = false
       variantBuilder.minSdk = 23
   }
}

在此阶段后,组件列表和将要创建产物都会被确定。

本示例的完整代码如下。如需更多此类示例,请查阅 Github gradle-recipes 仓库:

import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class CustomPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.tasks.register("hello"){ task->
            task.doLast {
                println("Hello " + project.parent?.name)
            }
        }

        val extension = project.extensions.getByName("androidComponents") as ApplicationAndroidComponentsExtension
        extension.beforeVariants { variantBuilder ->
            if (variantBuilder.name == "staging") {
                variantBuilder.enableUnitTest = false
                variantBuilder.minSdk = 23
            }
        }
        extension.finalizeDsl { ext->
            ext.buildTypes.create("staging").let { buildType ->
                buildType.initWith(ext.buildTypes.getByName("debug"))
                buildType.manifestPlaceholders["hostName"] = "internal.example.com"
                buildType.applicationIdSuffix = ".debugStaging"
                // 在后面解释 beforeVariants 时添加了本行代码。
                buildType.isDebuggable = true 
            }
        }
    }
}

总结

编写您自己的插件,您可以扩展 Android Gradle Plugin 并根据您的项目需求自定义您的构建!

在本文中,您已经了解了如何使用新的 Variant API 来在 AndroidComponentsExtension 中注册回调、使用 DSL 对象初始化 Variant、影响已被创建的 Variant,以及在 beforeVariants() 中它们的属性。

在下一篇文章中,我们将进一步介绍 Artifacts API,并向您展示如何从您的自定义 Task 中读取和转换产物。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io WebMaster | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.