Gradle 與 AGP 構建 API: 如何編寫外掛

Android開發者發表於2021-12-29

歡迎閱讀 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 中讀取和轉換產物。

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章