ในบทความนี้เราจะมาสร้าง Gradle Plugin ที่จะรวม Dependency หรือ Library ต่าง ๆ ที่ใช้บ่อย ๆ ในทุก Module ไม่ว่าจะเป็น App Module หรือ Library Module ก็ตาม ซึ่งเป็นเรื่องปกติที่เจอกันได้บ่อย ๆ ในโปรเจคที่มีขนาดใหญ่และมีการทำ Modularization
บทความในชุดเดียวกัน
- Introduction
- Android Gradle Plugin
- Getting Started
- [ตัวอย่าง] สร้าง Dependency Sharing Plugin เพื่อใช้กับทุก Module [Now Reading]
- [ตัวอย่าง] สร้าง Firebase Plugin เพื่อแยกคำสั่งของ Firebase ออกจาก App Module
- [ตัวอย่าง] สร้าง Configuration Sharing Plugin เพื่อใช้งานใน Library Module
สำหรับการขั้นตอนเริ่มต้นในการสร้าง Gradle Plugin จะอยู่ในบทความ "สร้าง Gradle Plugin ด้วย Kotlin เพื่อใช้งานบน Android - Getting Started" (แนะนำให้อ่านก่อน)
สมมติว่าในโปรเจคแอนดรอยด์ของเรามีหลาย Module และทุก Module จำเป็นต้องใช้ Dependency หรือ Library เหล่านี้เสมอ
// build.gradle.kts
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.activity:activity-ktx:1.8.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.cardview:cardview:1.0.0")
}
และแทนที่จะใส่ Dependency หรือ Library เหล่านี้ไว้ในทุก Module เราก็จะเปลี่ยนมาใช้เป็น Gradle Plugin แทน
เจ้าของบล็อกจึงสร้าง Gradle Plugin ไว้ใน buildSrc
และย้าย Dependency เหล่านั้นมาไว้ในนี้แทน
// buildSrc/src/main/kotlin/com/akexorcist/sleepingforless/gradleDependencySharingConventionPlugin.kt
package com.akexorcist.sleepingforless.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
class DependencySharingConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
dependencies {
"implementation"("androidx.core:core-ktx:1.12.0")
"implementation"("androidx.appcompat:appcompat:1.6.1")
"implementation"("androidx.activity:activity-ktx:1.8.0")
"implementation"("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
"implementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
"implementation"("com.google.android.material:material:1.8.0")
"implementation"("androidx.constraintlayout:constraintlayout:2.1.4")
"implementation"("androidx.recyclerview:recyclerview:1.3.1")
"implementation"("androidx.cardview:cardview:1.0.0")
}
}
}
}
ใน Override Method ที่ชื่อว่า apply
จะมี Argument หรือ Parameter ส่งเข้ามาเป็น target: Project
เพื่อใช้กำหนดค่า Build Script ให้กับ Module ที่เอา Plugin ตัวนี้ไปใช้
เจ้าของบล็อกจึงใช้ Scope Function ที่ชื่อว่า with
เพื่อทำให้ target
กลายเป็น this
เพื่อที่จะได้เรียกคำสั่งที่อยู่ในนั้นได้โดยตรง
// Without scope function
target.dependencies {
/* ... */
}
// With scope function
with(target) {
dependencies {
/* ... */
}
}
ซึ่งจะใช้แบบไหนก็ได้ผลลัพธ์เหมือนกัน อยู่ที่ความสะดวกในการเรียกใช้งานเท่านั้น
แต่ให้สังเกตตรงคำสั่ง implementation
ที่จะเปลี่ยนคำสั่งจากการใช้ Type-safe Model Accessors มาเป็น Dynamic Resolution
// Dynamic Resolution
"implementation"("androidx.core:core-ktx:1.12.0")
// Type-safe Model Accessors
implementation("androidx.core:core-ktx:1.12.0")
นั่นก็เพราะว่าคำสั่ง implementation
ที่ใช้ใน build.gradle.kts
นั้นจะเป็น Type-safe Model Accessors ที่ถูกสร้างขึ้นมาตอน Precompiled Script หรือก็คือพร้อม ๆ กับ buildSrc
จึงทำให้ไม่สามารถเรียกใช้งานในนี้ได้
ดังนั้นถ้าอยากได้คำสั่งแบบเดียวกับ Type-safe Model Accessors ก็จะต้องสร้าง Extension Function เพื่อใช้ใน buildSrc
แบบนี้แทน
// DependencySharingConventionPlugin.kt
class DependencySharingConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.activity:activity-ktx:1.8.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.cardview:cardview:1.0.0")
}
}
}
}
private fun DependencyHandler.`implementation`(dependencyNotation: Any): Dependency? =
add("implementation", dependencyNotation)
ใช่ครับ คำสั่งนี้ไปก๊อปมาจาก ImplementationConfigurationAccessors.kt
ของ Android Gradle Plugin นั่นเอง
ดังนั้นอยากจะใช้แบบไหนก็เลือกได้ตามใจชอบ เพราะไม่ว่าจะแบบไหนก็ให้ผลลัพธ์ที่เหมือนกันอยู่ดี
และขั้นตอนสำคัญที่ขาดไปไม่ได้สำหรับการสร้าง Gradle Plugin แบบนี้ก็คือ จะต้องเพิ่ม Plugin ตัวนี้ไว้ใน build.gradle.kts
ของ buildSrc
ด้วย
// build.gradle.kts (buildSrc)
/* ... */
gradlePlugin {
plugins {
register("dependencySharingConventionPlugin") {
id = "akexorcist.dependency-sharing.convention"
implementationClass = "com.akexorcist.sleepingforless.gradle.DependencySharingConventionPlugin"
}
}
}
เพียงเท่านี้ Gradle Plugin ของเราก็พร้อมนำไปใช้งานแล้ว
ไม่ว่าจะสร้าง Module เยอะแค่ไหน เป็น App Module หรือ Library Module ก็ตาม เพียงแค่เพิ่ม Plugin ID ของเราเข้าไปใน build.gradle.kts
ของ Module นั้น ๆ ก็จะทำให้ Module มี Dependency หรือ Library ตามที่เรากำหนดไว้ใน Gradle Plugin ของเราโดยทันที
// build.gradle.kts (App Module / Library Module)
plugins {
/* ... */
id("akexorcist.dependency-sharing.convention")
}
/* ... */
dependencies {
// ใส่เฉพาะ Dependency หรือ Library ที่ใช้แค่ในบาง Module
}
เพียงเท่านี้ก็เป็นอันเสร็จเรียบร้อย! ไม่ยากเลยใช่มั้ยล่ะ