ถึงแม้ว่า Android Studio เวอร์ชันล่าสุดจะเปลี่ยนไปใช้ Gradle Kotlin DSL เป็นค่าตั้งต้นเมื่อสร้างโปรเจคใหม่แล้วก็ตาม แต่ปัญหาสำหรับนักพัฒนาแอนดรอยด์ที่ยังใช้ Groovy อยู่ และไม่สะดวกย้ายไปใช้ Kotlin แทนก็เพราะว่าต้องทำงานกับโปรเจคเก่าที่เป็น Groovy ที่เขียน Build Script เพิ่มเข้าไปพอสมควร ทำให้เวลาย้ายไปใช้ Kotlin ก็จะต้องแก้โค้ดเหล่านั้นด้วย
ดังนั้นในบทความนี้จะมาช่วยให้นักพัฒนาเปลี่ยนโค้ดใน Gradle จากเดิมที่เป็น Groovy ให้กลายเป็น Kotlin กันครับ
บทความที่เกี่ยวข้อง
- Introduction
- Migration [Now Reading]
แนะนำให้อ่านบทความก่อนหน้าเพื่อทำความเข้าใจเกี่ยวกับ 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 ด้วย ถ้าเจอปัญหานอกเหนือจากที่บทความนี้พูดถึง ก็สามารถเล่าสู่กันฟังได้นะครับ