ถึงแม้ว่า Android Studio เวอร์ชันล่าสุดจะเปลี่ยนไปใช้ Gradle Kotlin DSL เป็นค่าตั้งต้นเมื่อสร้างโปรเจคใหม่แล้วก็ตาม แต่ปัญหาสำหรับนักพัฒนาแอนดรอยด์ที่ยังใช้ Groovy อยู่ และไม่สะดวกย้ายไปใช้ Kotlin แทนก็เพราะว่าต้องทำงานกับโปรเจคเก่าที่เป็น Groovy ที่เขียน Build Script เพิ่มเข้าไปพอสมควร ทำให้เวลาย้ายไปใช้ Kotlin ก็จะต้องแก้โค้ดเหล่านั้นด้วย

ดังนั้นในบทความนี้จะมาช่วยให้นักพัฒนาเปลี่ยนโค้ดใน Gradle จากเดิมที่เป็น Groovy ให้กลายเป็น Kotlin กันครับ

บทความที่เกี่ยวข้อง

แนะนำให้อ่านบทความก่อนหน้าเพื่อทำความเข้าใจเกี่ยวกับ Gradle Kotlin DSL

และเพื่อไม่ให้เป็นการเสียเวลา ขอเข้าเรื่องการเปลี่ยนโค้ด Groovy DSL ในแต่ละส่วนของ Gradle Build Script ให้เป็น Kotlin DSL กันเลย

Basic Syntax

สำหรับคำสั่งของ Gradle Build Script ทั่วไปที่เป็น Groovy สามารถแปลงให้กลายเป็น Kotlin ได้ไม่ยาก ขึ้นอยู่กับว่าเป็น Variable หรือ Method

ยกตัวอย่างเช่น

include ':library'
minifyEnabled false
buildConfigField "String", "ENVIRONMENT", "staging"

เมื่อแปลงเป็น Kotlin ก็จะได้เป็นแบบนี้แทน

include(":library")
minifyEnabled = false
buildConfigField("String", "ENVIRONMENT", "staging")

Gradle Plugin Repository

เนื่องจากการสร้างโปรเจคใหม่ในยุคหลังมานี้จะย้ายไปประกาศไว้ใน settings.gradle แบบนี้แทนแล้ว แต่ถ้าโปรเจคยังใช้วิธีประกาศไว้ใน build.gradle ของ Project-level แบบนี้อยู่

// build.gradle (Project-level)
buildscript {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

ก็ให้ย้ายคำสั่งเหล่านี้ไปไว้ใน settings.gradle เสียก่อน

// settings.gradle
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
ไม่จำเป็นต้องย้าย Class Path ตามมาด้วย

สำหรับการแปลงเป็น Kotlin แทบจะไม่ต้องแก้ไขอะไร เพราะคำสั่งในส่วนนี้จะใช้ Syntax แบบเดียวกับ Groovy เลย

// settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

Gradle Plugin ID

โดยปกติแล้ว นักพัฒนาจะประกาศ Gradle Plugin ID ที่ต้องการใช้งานไว้ใน Project-level และเรียกใช้งานใน Module-level ในรูปแบบของ Plugin DSL แบบนี้

// build.gradle (Project-level)
plugins {
    id 'com.android.application' version '8.1.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
    id 'com.android.library' version '8.1.0' apply false
}

// build.gradle (Module-level)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

เมื่อเปลี่ยนเป็น Kotlin ก็จะได้เป็นแบบนี้แทน

// build.gradle.kts (Project-level)
plugins {
    id("com.android.application") version "8.1.0" apply false
    id("org.jetbrains.kotlin.android") version "1.8.10" apply false
}

// build.gradle.kts (Module-level)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

สำหรับ Gradle Plugin ที่ยังใช้ Legacy Plugin Application

ถ้ามี Gradle Plugin ที่ใช้ Legacy Plugin Application อยู่ ขอแนะนำให้ย้ายไปใช้เป็น Plugin DSL ให้เรียบร้อยซะ

ยกตัวอย่างเช่น Gradle Plugin ของ Google Play Services ที่เป็นมรดกตกทอดอยู่ในโปรเจคมาเป็นระยะเวลายาวนานแบบนี้

// build.gradle (Project-level)
buildscript {
    dependencies {
        classpath "com.google.gms:google-services:4.3.15"
    }
}

// build.gradle (Module-level)
apply plugin: "com.google.gms.google-services"

ก็ให้เปลี่ยนเป็น Plugin DSL แทน

// build.gradle (Project-level)
plugins {
    id 'com.google.gms.google-services' version '4.3.15' apply false
}

// build.gradle (Module-level)
plugins {
    id 'com.google.gms.google-services'
}

จากนั้นก็ทำการเปลี่ยนเป็น Kotlin ต่อให้เรียบร้อย

// build.gradle (Project-level)
plugins {
    id("com.google.gms:google-services") version '4.3.15' apply false
}

// build.gradle (Module-level)
plugins {
    id("com.google.gms.google-services")
}

สำหรับ Local Gradle File

ในกรณีที่มีการสร้าง Gradle File ไว้ในโปรเจคและต้องการเรียกใช้งานในบาง Module

apply from: "${project.rootDir}/path/to/local_plugin.gradle"

แปลงเป็น Kotlin แบบนี้ได้เลย

// Groovy DSL file
apply(from = "${project.rootDir}/path/to/local_plugin.gradle")

// Kotlin DSL file
apply(from = "${project.rootDir}/path/to/local_plugin.gradle.kts")
แน่นอนว่า Gradle File จะเขียนด้วย Groovy หรือ Kotlin ก็ได้ เพราะเรียกใช้งานได้เหมือนกัน แต่การสร้าง Gradle File ด้วย Kotlin จะต้องประกาศ​ Gradle Plugin ที่ใช้ใน Gradle File นั้นด้วย ซึ่งต่างจาก Groovy ที่ไม่ต้องประกาศ​ เพราะ Gradle จะอิงจาก Build Script ที่เป็นคนเรียก Gradle File นั้นอีกที

Shorthand Plugin ID และ Kotlin Plugin Extension Function

Gradle Plugin บางตัวสามารถประกาศแบบ Shorthand ได้ด้วย ยกตัวอย่างเช่น Gradle Plugin ของ Kotlin

// Namespaced Plugin ID
id("org.jetbrains.kotlin.android")

// Shorthand Plugin ID
id("kotlin-android")
Shorthand Namespaced
kotlin org.jetbrains.kotlin.jvm
kotlin-android org.jetbrains.kotlin.android
kotlin-kapt org.jetbrains.kotlin.kapt
kotlin-parcelize org.jetbrains.kotlin.plugin.parcelize

และสำหรับ Kotlin DSL ก็มี Extension Function สำหรับ Gradle Plugin ให้ด้วยเช่นกัน

// build.gradle.kts (Project-level)
plugins {
    kotlin("jvm") version "1.9.10" apply false
    kotlin("android") version "8.1.0" apply false
    kotlin("plugin.parcelize") version "1.9.10" apply false
}

// build.gradle.kts (Module-level)
plugins {
    kotlin("jvm")
    kotlin("android")
    kotlin("plugin.parcelize")
}

แต่ไม่มี Extension Function สำหรับ Android นะ

Gradle Dependency Repository

สำหรับ Dependency Repository จะคล้ายกับ Plugin Repository ตรงที่จะย้ายไปประกาศไว้ใน settings.gradle เช่นกัน

// settings.gradle
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

ดังนั้นจึงสามารถเปลี่ยนเป็น Kotlin ได้ทันทีโดยไม่ต้องทำอะไรเพิ่มเช่นกัน

// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

แต่ถ้ามีการใช้ Custom/Private Dependency Repository แล้วจำเป็นต้องใช้ Gradle Properties เช่น เก็บ Credential สำหรับ Private Dependency Repository ไว้ใน local.properties หรือ ~/.gradle/gradle.properties ก็อาจจะติดปัญหาว่าดึงค่าเหล่านั้นจากใน settings.gradle ไม่ได้ ก็เลยต้องประกาศไว้ใน build.gradle ของ Project-level แบบนี้แทน

// build.gradle (Project-level)
allprojects {
    repositories {
        maven {
            String USERNAME = /* Get username from somewhere */
            String PASSWORD = /* Get password from somewhere */
            url "<private_repository_url>"
            credentials {
                username USERNAME
                password PASSWORD
            }
        }
    }
}

ก็ให้แปลงเป็น Kotlin แบบนี้ได้เลย

// build.gradle.kts (Project-level)
allprojects {
    repositories {
        maven {
            val username: String = /* Get username from somewhere */
            val password: String = /* Get password from somewhere */
            setUrl("<private_repository_url>")
            credentials {
                setUsername(username)
                setPassword(password)
            }
        }
    }
}

Build Variant

Build Type

ในกรณีที่มี Build Type นอกเหนือไปจาก debug และ release

// build.gradle (Module-level)
android {
    buildTypes {
        debug { /* ... */ }
        release { /* ... */ }
        googlePlay { /* ... */ }
        galaxyStore { /* ... */ }
        appGallery { /* ... */ }
    }
}

ให้ใช้คำสั่ง create เพื่อสร้าง Build Type ที่ต้องการแทน

// build.gradle.kts (Module-level)
android {
    buildTypes {
        debug { /* ... */ }
        release { /* ... */ }
        create("googlePlay") { /* ... */ }
        create("galaxyStore") { /* ... */ }
        create("appGallery") { /* ... */ }
    }
}

Product Flavor

ในกรณีที่มีการสร้าง Product Flavor ในโปรเจค

// build.gradle (Module-level)
android {
    flavorDimensions += "default"
    buildTypes {
        alpha { 
            dimension "default"
            /* ... */
        }
        beta { 
            dimension "default"
            /* ... */
        }
        production { 
            dimension "default"
            /* ... */
        }
    }
}

ให้ใช้คำสั่ง create เพื่อสร้าง Product Flavor ที่ต้องการแทน

// build.gradle.kts (Module-level)
android {
    flavorDimensions += "default"
    buildTypes {
        create("alpha") { 
            dimension = "default"
            /* ... */
        }
        create("beta") { 
            dimension = "default"
            /* ... */
        }
        create("production") { 
            dimension = "default"
            /* ... */
        }
    }
}

Module Dependencies

สำหรับ Dependencies ใด ๆ ใน Module-level ไม่ว่าจะเป็น Dependencies ในรูปแบบไหนก็ตาม

// build.gradle (Module-level)
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation project(":library")
    implementation 'androidx.core:core-ktx:1.10.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    debugImplementation 'androidx.compose.ui:ui-tooling'
    betaDebugImplementation 'com.github.chuckerteam.chucker:library:4.0.0'
}

สามารถแปลงให้เป็น Kotlin ในรูปแบบนี้ได้เลย

// build.gradle.kts (Module-level)
dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    implementation(project(":library"))
    implementation("androidx.core:core-ktx:1.10.1")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    debugImplementation("androidx.compose.ui:ui-tooling")
    "betaDebugImplementation"("com.github.chuckerteam.chucker:library:4.0.0")
}
ไม่ว่าจะเป็น implementation, api, kapt, หรือ ksp ก็จะมีลักษณะแบบเดียวกันทั้งหมด

จะเห็นว่าถ้าเป็น Build Variant หรือ Product Flavor ที่สร้างขึ้นมาเองจะใช้คำสั่งแบบ Dynamic Type แทน ต่างจากปกติที่มีคำสั่งเตรียมไว้ให้แล้ว ดังนั้นจะสร้างเป็น Extension Function เก็บไว้ใช้หลาย ๆ ที่แบบนี้ก็ได้เช่นกัน

fun DependencyHandler.betaDebugImplementation(dependencyNotation: Any): Dependency? =
    add("betaDebugImplementation", dependencyNotation)

dependencies {
    betaDebugImplementation("com.github.chuckerteam.chucker:library:4.0.0")
}

Android Configuration

Signing Configs

สำหรับ Signing Config ที่เอาไว้กำหนดค่าต่าง ๆ ของ Keystore เพื่อทำ App Signing

// build.gradle (Module-level)
android {
    Properties properties = new Properties()
    properties.load(project.rootProject.file("local.properties").newDataInputStream())

    signingConfigs {
        release_key {
            keyAlias properties['key_alias']
            keyPassword properties['key_password']
            storeFile file(properties['store_file'])
            storePassword properties['store_password']
        }
    }
}

เมื่อแปลงเป็น Kotlin จะกลายเป็นแบบนี้

// build.gradle.kts (Module-level)
android {
    val properties = Properties().apply {
        load(project.rootProject.file("local.properties").inputStream()
    }

    signingConfigs {
        create("release_key") {
            storeFile = file(properties.getProperty("store_file"))
            storePassword = properties.getProperty("store_password")
            keyAlias = properties.getProperty("key_alias")
            keyPassword = properties.getProperty("key_password")
        }
    }
}

Compile Options

สำหรับ Compile Options ที่เอาไว้กำหนดค่าต่าง ๆ ในตอน Compile

// build.gradle (Module-level)
android {
    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

เมื่อแปลงเป็น Kotlin จะกลายเป็นแบบนี้

// build.gradle (Module-level)
android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

Kotlin Options

สำหรับ Kotlin Options ที่เอาไว้กำหนด JVM Target สำหรับ Kotlin

// build.gradle (Module-level)
android {
    kotlinOptions {
        jvmTarget = "17"
    }
}

สามารถแปลงเป็น Kotlin ได้ทันทีโดยไม่ต้องเปลี่ยนอะไร

// build.gradle (Module-level)
android {
    kotlinOptions {
        jvmTarget = "17"
    }
}

Build Features

สำหรับ Build Features ที่เอาไว้เปิดใช้งานฟีเจอร์อย่าง ViewBinding, RenderScript, หรือ Compose

// build.gradle (Module-level)
android {
    buildFeatures {
        viewBinding true
        compose true
    }
}

เมื่อแปลงเป็น Kotlin จะกลายเป็นแบบนี้

// build.gradle (Module-level)
android {
    buildFeatures {
        viewBinding = true
        compose = true
    }
}

Compose Options

สำหรับ Compose Options ที่เอาไว้กำหนดค่าต่าง ๆ ของ Jetpack Compose

// build.gradle (Module-level)
android {
    composeOptions {
        kotlinCompilerExtensionVersion '1.4.3'
    }
}

เมื่อแปลงเป็น Kotlin จะกลายเป็นแบบนี้

// build.gradle (Module-level)
android {
    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.3"
    }
}

Packaging

สำหรับ Packaging ที่เอาไว้กำหนดค่าต่าง ๆ ในขั้นตอนการรวมไฟล์ข้อมูล (Packaging) ให้กลายเป็น APK หรือ AAB

// build.gradle (Module-level)
android {
    packaging {
        resources.excludes += '/META-INF/{AL2.0,LGPL2.1}'
    }
}

เมื่อแปลงเป็น Kotlin จะกลายเป็นแบบนี้

// build.gradle (Module-level)
android {
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

Test Options

สำหรับ Test Options ที่เอาไว้กำหนดค่าต่าง ๆ สำหรับ Local Test และ Instrumented Test

// build.gradle (Module-level)
android {
    testOptions {
        unitTests {
            includeAndroidResources true
            returnDefaultValues true
        }
        animationsDisabled true
    }
}

เมื่อแปลงเป็น Kotlin จะกลายเป็นแบบนี้

// build.gradle (Module-level)
android {
    testOptions {
        unitTests {
            isIncludeAndroidResources = true
            isReturnDefaultValues = true
        }
        animationsDisabled = true
    }
}

Source Sets

สำหรับ Source Sets ที่เอาไว้กำหนด Source File Directory (เช่น Java Source, Kotlin Source, Android Resource, Android Manifest เป็นต้น) ให้กับ Gradle

// build.gradle (Module-level)
android {
    sourceSets {
        main {
            kotlin.srcDirs += 'src/main/akexorcist'
        }
        test {
            kotlin.srcDirs += 'src/test/akexorcist'
        }
        androidTest {
            kotlin.srcDirs += 'src/androidTest/akexorcist'
        }
    }
}

สามารถแปลงเป็น Kotlin ได้เป็นแบบนี้

// build.gradle (Module-level)
android {
    sourceSets {
        getByName("main") {
            kotlin.srcDir("src/main/akexorcist")
        }
        getByName("test") {
            kotlin.srcDir("src/test/akexorcist")
        }
        getByName("androidTest") {
            kotlin.srcDir("src/androidTest/akexorcist")
        }
    }
}

สรุป

จะเห็นว่าขั้นตอนการย้ายโค้ดจาก Groovy DSL ไปเป็น Kotlin DSL ใน Gradle นั้นไม่ได้มีความซับซ้อนอย่างที่คิด แต่อาจจะมีบางคำสั่งใน Groovy ที่ต้องปรับให้เป็นวิธีใหม่เสียก่อน และในบางคำสั่งก็อาจจะต้องใช้วิธีอื่นที่แตกต่างจากปกติ เพื่อให้คำสั่ง Groovy เดิมที่นักพัฒนาเคยเขียนเพิ่มเข้าไปยังคงทำงานได้ปกติเมื่อย้ายมาเป็น Kotlin

ทั้งนี้ทั้งนั้นก็ขึ้นอยู่กับโค้ด Groovy ที่นักพัฒนาเขียนเพิ่มเข้าไปใน Gradle ด้วย ถ้าเจอปัญหานอกเหนือจากที่บทความนี้พูดถึง ก็สามารถเล่าสู่กันฟังได้นะครับ

แหล่งข้อมูลอ้างอิง