สร้าง Gradle Plugin ด้วย Kotlin เพื่อใช้งานบน Android - การสร้าง Configuration Sharing Plugin เพื่อใช้งานใน Library Module
Gradle Plugin ในบทความนี้จะเหมาะกับโปรเจคแอนดรอยด์ที่มีการทำ App Modularization ที่แบ่งโค้ดและการทำงานแยกเป็นหลาย Module เพราะโดยปกติแล้ว Library Module แต่ละตัวมักจะมีโค้ดของ Gradle ที่คล้ายกันทำให้เราสร้าง Gradle Plugin เพื่อแชร์โค้ดตรงจุดนี้ได้
บทความในชุดเดียวกัน
- Introduction
- Android Gradle Plugin
- Getting Started
- [ตัวอย่าง] สร้าง Dependency Sharing Plugin เพื่อใช้กับทุก Module
- [ตัวอย่าง] สร้าง Firebase Plugin เพื่อแยกคำสั่งของ Firebase ออกจาก App Module
- [ตัวอย่าง] สร้าง Configuration Sharing Plugin เพื่อใช้งานใน Library Module [Now Reading]
สำหรับการขั้นตอนเริ่มต้นในการสร้าง Gradle Plugin จะอยู่ในบทความ "สร้าง Gradle Plugin ด้วย Kotlin เพื่อใช้งานบน Android - Getting Started" (แนะนำให้อ่านก่อน)
มาเริ่มกันเถอะ
สมมติว่าเรามี Library Module หลายตัวที่มีโค้ด Gradle แบบนี้
// build.gradle.kts (Library Module)
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.akexorcist.feature.a"
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildFeatures {
viewBinding = true
}
buildTypes {
debug {
isMinifyEnabled = false
}
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
โดยเราจะย้าย Build Script บางส่วนไปไว้ใน Gradle Plugin แทน ยกเว้น
namespace
: ที่แต่ละ Library Module จะต้องกำหนดค่าต่างกันdependencies
: ที่แต่ละ Library Module อาจจะมี Dependency หรือ Library ที่ใช้งานแตกต่างกันไป หรือถ้า Library Module ทุกตัวมี Dependency ที่ใช้งานเหมือนกับบางส่วน ก็ทำเป็น Gradle Plugin แยกอีกตัวแบบบทความก่อนหน้าก็ได้เช่นกัน ไม่จำเป็นต้องรวมทั้งหมดไว้ใน Gradle Plugin ตัวเดียวเสมอไป
ดังนั้นเจ้าของบล็อกจึงสร้าง Gradle Plugin ชื่อว่า LibraryConventionPlugin
พร้อมกับเตรียมโค้ดเบื้องต้นไว้แบบนี้
package com.akexorcist.sleepingforless.gradle
import com.android.build.api.dsl.ApplicationExtension
import com.google.firebase.perf.plugin.FirebasePerfExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
class LibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
// Add build logic for library module here
}
}
}
โดยเริ่มจากการเพิ่ม Gradle Plugin ที่ต้องใช้ใน Library Module เสียก่อน
// buildSrc/src/main/kotlin/com/akexorcist/sleepingforless/gradle/LibraryConventionPlugin.kt
class LibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
}
}
}
จากนั้นให้เรียกใช้ LibraryExtension
เพื่อกำหนดค่าสำหรับ Library Module แบบนี้
// buildSrc/src/main/kotlin/com/akexorcist/sleepingforless/gradle/LibraryConventionPlugin.kt
/* ... */
import com.android.build.gradle.LibraryExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
class LibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<LibraryExtension> {
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildFeatures {
viewBinding = true
}
buildTypes {
debug {
isMinifyEnabled = false
}
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
}
}
}
}
}
จะเห็นว่าคำสั่งส่วนใหญ่ที่ใช้ใน Library Module มักจะเรียกใช้งานผ่าน LibraryExtension
ได้เลย จะมีแค่เพียง kotlinOptions {..}
ที่ไม่ใช่คำสั่งของ LibraryExtension
โดยตรง แต่เป็น Extension Function ของ KotlinJvmOptions
ที่เรียกใช้งานจากในนี้ไม่ได้ จึงต้องเปลี่ยนมากำหนดค่าผ่าน Task ที่ชื่อว่า KotlinCompile
แทน
จะเรียกใช้KotlinCompile
ข้างนอกหรือข้างในLibraryExtension
ก็ให้ผลเหมือนกัน เพราะกำหนดค่าโดยหา Task ที่อยู่ในโปรเจค ไม่ใช่ใน Library Module
โดยโค้ดทั้งหมดที่อยู่ใน LibraryConventionPlugin
จะมีดังนี้
// buildSrc/src/main/kotlin/com/akexorcist/sleepingforless/gradle/LibraryConventionPlugin.kt
package com.akexorcist.sleepingforless.gradle
import com.android.build.gradle.LibraryExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
class LibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<LibraryExtension> {
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildFeatures {
viewBinding = true
}
buildTypes {
debug {
isMinifyEnabled = false
}
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
}
}
}
}
}
และที่ขาดไปไม่ได้เลยก็คือการประกาศใน build.gradle.kts
ของ buildSrc
เพื่อให้รู้จัก Gradle Plugin ของเรานั่นเอง
// build.gradle.kts (buildSrc)
/* ... */
gradlePlugin {
plugins {
register("libraryConventionPlugin") {
id = "akexorcist.library.convention"
implementationClass = "com.akexorcist.sleepingforless.gradle.LibraryConventionPlugin"
}
}
}
เพียงเท่านี้ Gradle Plugin ตัวนี้ก็พร้อมนำไปใช้งานกับ Library Module ทั้งหมดในโปรเจคแล้ว และ Build Script ที่อยู่ใน Library Module ก็จะเหลือแค่นี้แทน
// build.gradle.kts (Library Module)
plugins {
id("akexorcist.library.convention")
}
android {
namespace = "com.akexorcist.feature.a"
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
จะเห็นว่าการทำ Gradle Plugin แบบนี้จะช่วยให้ Build Script ใน Library Module มีความสั้นกระชับมากขึ้น และถ้าต้องการกำหนดค่าเพิ่มเติมให้กับบาง Module ก็สามารถทำใน build.gradle.kts
ของ Library Module ได้ตามปกติ เพราะค่าที่กำหนดไว้ใน Gradle Plugin จะถูกแทนที่ด้วยโค้ดใน build.gradle.kts
แทน
สรุป
การ Gradle Plugin สำหรับ Library Module เพื่อใช้งานกับโปรเจคที่มีการแบ่ง Module มากมายเพื่อทำ App Modularization จะช่วยลดโค้ดซ้ำซ้อนที่จะต้องอยู่ใน build.gradle.kts
ของทุก Library Module
และเมื่อเรารวม Build Script บางส่วนที่เรียกใช้งานเหมือนกันใน Library Module ทุกตัวให้รูปของ Gradle Plugin ได้ ก็จะช่วยให้ใน build.gradle.kts
ของ Library Module แต่ละตัวเหลือแต่โค้ดที่จำเป็นต้องกำหนดแยกสำหรับแต่ละ Module เท่านั้น ทำให้การดูแลโค้ดในแต่ละ Module ทำได้ง่ายกว่า
แน่นอนว่าเราสามารถใช้แนวคิดนี้เพื่อทำ Gradle Plugin ที่รองรับทั้ง App Module และ Library Module ก็ได้เช่นกัน แต่ก็อย่าลืมว่า App Module มีแค่เพียงตัวเดียวในโปรเจค และมี Build Script ที่แตกต่างกับ Library Module เล็กน้อย ถ้าต้องการสร้าง Gradle Plugin เพื่อใช้ Build Script บางส่วนร่วมกัน ก็แนะนำให้ทำ Gradle Plugin แยกกันระหว่าง App Module กับ Library Module ดีกว่า แล้วทำ Function หรือ Extension Function เพื่อใช้งานโค้ดส่วนนั้นร่วมกันแทนดีกว่า