ในบทความนี้เราจะมาสร้าง Gradle Plugin ที่จะรวม Dependency หรือ Library ต่าง ๆ ที่ใช้บ่อย ๆ ในทุก Module ไม่ว่าจะเป็น App Module หรือ Library Module ก็ตาม ซึ่งเป็นเรื่องปกติที่เจอกันได้บ่อย ๆ ในโปรเจคที่มีขนาดใหญ่และมีการทำ Modularization

บทความในชุดเดียวกัน

สำหรับการขั้นตอนเริ่มต้นในการสร้าง Gradle Plugin จะอยู่ในบทความ "สร้าง Gradle Plugin ด้วย Kotlin เพื่อใช้งานบน Android - Getting Started" (แนะนำให้อ่านก่อน)

สร้าง Gradle Plugin ด้วย Kotlin เพื่อใช้งานบน Android - Getting Started
หลังจากเข้าใจโครงสร้างของ Android Gradle Plugin เบื้องต้นแล้ว สิ่งที่ต้องทำก่อนที่จะเขียน Gradle Plugin หรือ Convention Plugin ก็คือการเตรียมโปรเจคให้พร้อมเสียก่อน

สมมติว่าในโปรเจคแอนดรอยด์ของเรามีหลาย 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
}

เพียงเท่านี้ก็เป็นอันเสร็จเรียบร้อย! ไม่ยากเลยใช่มั้ยล่ะ