ก่อนจะเริ่มเขียน Gradle Plugin ด้วย Kotlin เราควรจะเข้าใจโครงสร้างของ Android Gradle Plugin ที่เราใช้งานใน Gradle กันอยู่ทุกวันนี้เสียก่อน
บทความในชุดเดียวกัน
- Introduction
- Android Gradle Plugin [Now Reading]
- Getting Started
- [ตัวอย่าง] สร้าง Dependency Sharing Plugin เพื่อใช้กับทุก Module
- [ตัวอย่าง] สร้าง Firebase Plugin เพื่อแยกคำสั่งของ Firebase ออกจาก App Module
- [ตัวอย่าง] สร้าง Configuration Sharing Plugin เพื่อใช้งานใน Library Module
ที่มาของคำสั่ง Gradle สำหรับ App Module และ Library Module
เมื่อลองดูโค้ดใน build.gradle.kts
ของ App Module ก็จะเห็นว่าในนั้นมี Block ต่าง ๆ ที่ประกาศไว้ ซึ่งเป็น Extension Function ที่ Gradle Plugin แต่ละตัวได้เตรียมไว้ให้นั่นเอง
// build.gradle.kts (App Module)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.akexorcist.sleepingforless"
compileSdk = 33
/* ... */
}
/* ... */
ยกตัวอย่างเช่น คำสั่ง android {..}
จะมาจาก Plugin ที่ชื่อว่า com.android.application
ที่ประกาศไว้ใน plugins {..}
ดังนั้นถ้าไม่ใส่ Plugin ตัวนี้ก็จะเรียกใช้งานคำสั่งนั้นไม่ได้
และเมื่อลองกดเข้าไปดูข้างในคำสั่ง android {..}
ดูก็จะพบว่าข้างในนั้นเป็นแค่ Extension Function ที่ com.android.application
ได้เตรียมไว้ให้
fun org.gradle.api.Project.`android`(configure: Action<com.android.build.gradle.internal.dsl.BaseAppModuleExtension>): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)
การกดไล่โค้ดใน Gradle ที่ใช้ Kotlin DSL แบบนี้คือข้อดีอย่างนึงที่ทำใน Groovy DSL ไม่ค่อยได้ เป็นอีกเหตุผลที่เจ้าของบล็อกเลือกที่จะใช้ Kotlin DSL มากกว่า
และภายใน Block ของ android {..}
ก็จะเป็น BaseAppModuleExtension
เพื่อให้เราเรียกใช้คำสั่งต่าง ๆ อย่าง namespace
, compileSdk
หรือ defaultConfig
เพื่อกำหนดค่าให้กับโปรเจคนั่นเอง
ในขณะเดียวกัน com.android.library
สำหรับ Library Module ก็ทำงานในลักษณะเดียวกัน แต่จะได้เป็น LibraryExtension
แทน ไม่ใช่ BaseAppModuleExtension
เนื่องจากตั้งค่าบางตัวแบบ App Module ไม่ได้ (เช่น applicationId
)
เมื่อลองดู BaseAppModuleExtension
กับ LibraryExtension
ก็จะพบว่าทั้งคู่เป็น Sub Class ของ CommonExtension
เหมือนกัน
CommonExtension
จะเป็น Interface ที่ประกอบไปด้วย Generic ที่เป็น Interface อีก 5 ตัวคือ BuildFeature
, BuildType
, DefaultConfig
, ProductFlavor
, และ AndroidResources
interface CommonExtension<
BuildFeaturesT : BuildFeatures,
BuildTypeT : BuildType,
DefaultConfigT : DefaultConfig,
ProductFlavorT : ProductFlavor,
AndroidResourcesT : AndroidResources> { /* ... */ }
โดยที่ BaseAppModuleExtension
กับ LibraryExtension
จะกำหนด Generic ที่เป็น Interface ทั้ง 5 ตัวแตกต่างกันแบบนี้
CommonExtension | BaseAppModuleExtension | LibraryExtension |
BuildFeatures | ApplicationBuildFeatures | LibraryBuildFeatures |
BuildType | ApplicationBuildType | LibraryBuildType |
DefaultConfig | ApplicationDefaultConfig | LibraryDefaultConfig |
ProductFlavor | ApplicationProductFlavor | LibraryProductFlavor |
AndroidResources | ApplicationAndroidResources | LibraryAndroidResources |
ซึ่ง Interface เหล่านี้ก็คือที่มาของค่าบางส่วนที่นักพัฒนาสามารถกำหนดใน build.gradle.kts
ของ App Module และ Library Module ได้นั่นเอง
// build.gradle.kts (App Module)
android {
buildFeatures { /* ... */ }
buildType { /* ... */ }
defaultConfig { /* ... */ }
productFlavor { /* ... */ }
androidResources { /* ... */ }
}
// build.gradle.kts (Library Module)
android {
buildFeatures { /* ... */ }
buildType { /* ... */ }
defaultConfig { /* ... */ }
productFlavor { /* ... */ }
androidResources { /* ... */ }
}
ทำให้เวลากำหนดค่าใน android {..}
ของ App Module กับ Library Module จะมีค่าให้กำหนดไม่เท่ากัน เพราะค่าบางอย่างใน App Module ก็ไม่จำเป็นต้องกำหนดใน Library Module
และถ้าลองดูโค้ดข้างใน CommonExtension
ก็จะพบว่ามีค่าอื่น ๆ อยู่ด้วย จึงเป็นที่มาว่าค่าบางอย่างก็กำหนดได้ทั้งใน App Module และ Library Module
// CommonExtension
interface CommonExtension</* ... */> {
var compileSdk: Int?
var namespace: String?
val compileOptions: CompileOptions
fun compileOptions(action: CompileOptions.() -> Unit)
val jacoco: JacocoOptions
fun jacoco(action: JacocoOptions.() -> Unit)
val testCoverage: TestCoverage
fun testCoverage(action: TestCoverage.() -> Unit)
val packaging: Packaging
fun packaging(action: Packaging.() -> Unit)
val signingConfigs: NamedDomainObjectContainer<out ApkSigningConfig>
fun signingConfigs(action: NamedDomainObjectContainer<out ApkSigningConfig>.() -> Unit)
val composeOptions: ComposeOptionsfun composeOptions(action: ComposeOptions.() -> Unit)
/* ... */
}
ดังนั้นการเข้าใจความสัมพันธ์ของคลาสต่าง ๆ ที่เกี่ยวข้องกับ CommonExtension
ก็จะช่วยให้เราเข้าใจที่มาของคำสั่งสำหรับแอนดรอยด์ใน build.gradle.kts
ได้ง่ายขึ้น ซึ่งจะทำให้เราสามารถเขียน Gradle Plugin เพื่อจัดการกับการทำงานได้ส่วนนี้ได้ง่ายขึ้นด้วยเช่นกัน
3rd Party Gradle Plugin ก็ทำงานในลักษณะเดียวกัน
ยกตัวอย่างเช่น Gradle Android Test Aggregation Plugin ของ gmazzo ที่ใช้รวม Test Report ของ Test Result และ Test Coverage ของทุก Module ให้กลายเป็นไฟล์เดียวกัน ที่จะมีคำสั่งของ Gradle เพื่อให้นักพัฒนากำหนดการทำงานของ Plugin ตัวนี้ผ่าน build.gradle.kts
ที่อยู่ใน Project Level และ Module Level ได้
// build.gradle.kts (Project)
plugins {
id("io.github.gmazzo.test.aggregation.coverage") version "<latest>"
id("io.github.gmazzo.test.aggregation.results") version "<latest>"
}
testAggregation {
modules { include(project(":app")) }
coverage { include("com/**/Login*") }
}
// build.gradle.kts (App Module)
android {
/* ... */
productFlavors {
create("stage") {
aggregateTestCoverage.set(true)
}
create("prod") {
aggregateTestCoverage.set(false)
}
}
}
ที่ทำแบบนี้ได้ก็เพราะว่าผู้สร้าง Plugin ได้สร้าง Extension Function สำหรับ Project Level และ Module Level เตรียมไว้ให้แบบนี้
// https://github.com/gmazzo/gradle-android-test-aggregation-plugin/blob/main/plugin/src/main/kotlin/io/github/gmazzo/android/test/aggregation/InternalDSL.kt#L19-L24C10
internal val Project.testAggregationExtension: TestAggregationExtension
get() = extensions.findByType()
?: extensions.create<TestAggregationExtension>("testAggregation").apply {
modules.includes.finalizeValueOnRead()
modules.excludes.finalizeValueOnRead()
}
// https://github.com/gmazzo/gradle-android-test-aggregation-plugin/blob/main/plugin/src/main/kotlin/org/gradle/kotlin/dsl/TestAggregationDSL.kt#L24-L28
val BuildType.aggregateTestCoverage: Property<Boolean>
get() = extensions.getByName<Property<Boolean>>(::aggregateTestCoverage.name)
val ProductFlavor.aggregateTestCoverage: Property<Boolean>
get() = extensions.getByName<Property<Boolean>>(::aggregateTestCoverage.name)
ดังนั้นเมื่อเราดู Extension Function ที่ Plugin ตัวนี้ได้เตรียมไว้ให้ ก็จะทำให้รู้ได้ทันทีว่าคำสั่ง testAggregation
ใช้ได้เฉพาะใน Project Level เท่านั้น ส่วนคำสั่ง aggregateTestCoverage
สามารถใช้ใน buildType {..}
หรือ productFlavor {..}
ก็ได้
Android Base Plugin
โดยปกติแล้วถ้าเป็น App Module จะต้องใช้ Plugin ของ Android Gradle Plugin ที่ชื่อว่า com.android.application
และถ้าเป็น Library Module ก็จะต้องใช้ com.android.library
// build.gradle.kts (App Module)
plugins {
id("com.android.application")
}
// build.gradle.kts (Library Module)
plugins {
id("com.android.library")
}
แต่ในการสร้าง Gradle Plugin บางครั้งก็อาจจะต้องการใช้คำสั่งของ Android Gradle Plugin โดยที่ไม่ต้องการกำหนดว่าเป็น App Module หรือ Library Module
ในกรณีนี้เราสามารถใช้ Plugin ID ที่ชื่อว่า com.android.base
แทนได้ ซึ่งเป็น Base Plugin ของ Android Gradle Plugin ที่ไม่มีผลใด ๆ กับ Module ที่นำไปใช้ ซึ่งเราจะได้เห็นและเข้าใจวิธีการใช้ Plugin ID ตัวนี้ได้ในบทความถัด ๆ ไป
สรุป
Android Gradle Plugin เป็น Gradle Plugin ที่รวม Plugin และชุดคำสั่งต่าง ๆ ไว้เพื่อให้นักพัฒนาใช้งานใน Build Script ไม่ว่าจะเป็น build.gradle.kts
ที่อยู่ใน App Module หรือ Library Module ก็ตาม ซึ่งเป็นส่วนสำคัญที่เราจะต้องเข้าใจเบื้องหลังของมันเล็กน้อย เพื่อใช้ในการสร้าง Gradle Plugin
เมื่อเราเข้าใจการทำงานของ Android Gradle Plugin และวิธีที่ 3rd Party Gradle Plugin ใช้ เราก็จะสามารถสร้าง Gradle Plugin ด้วย Kotlin เพื่อใช้งานในโปรเจคแอนดรอยด์ของเราเองได้ไม่ยากแล้ว