รวมมิตรอัพเดตแพชครั้งใหญ่ กับ Kotlin DSL กับ toml

Android Apr 18, 2024

เราได้มีโอกาสได้ช่วยทำแอพรวมมิจ ที่เป็นข่าวตามสื่อต่าง ๆ ที่มี 9arm เป็นต้นคิด และพี่เอก Android GDE เป็นคน implement หลัก

แต่เนื่องจากโปรเจกต์นี้ feature หลักได้มีการ implement ไปแล้ว เราเลยหยิบ feature Open Source Licenses ที่ทำง่ายสุดแล้วนั้น

แต่ด้วยความที่เป็นโปรเจกต์ใหม่ ตัว UI ทำด้วย Jetpack Compose ด้วย แล้วมีการ setting dependency แบบใหม่ ที่เราเองก็ทำโปรเจกต์ที่มีอายุพอสมควร ยังไม่ได้ใช้ของใหม่หมด เลยต้องมาเรียนรู้กับเรื่อง Kotlin DSL โอเคมันแค่สลับตำแหน่งจากของเดิม แต่ toml นี่สินะ สิ่งที่ต้องเรียนรู้ใหม่ในครั้งนี้

ในบล็อกนี้เลยมาเล่าการใช้งาน toml กับ Kotlin DSL กัน ว่ามันแตกต่างจากเดิมยังไง โดยอธิบายผ่านโปรเจกต์รวมมิจนี่แหละ

ทำความรู้จัก Gradle กันก่อน

ในการ build โปรเจกต์ Android application นั้น เราจะมี gradle ในการช่วยจัดการการ build ให้ง่ายขึ้น ซึ่งมันเป็น build system file ที่เราสามารถเขียนโค้ดกำหนดรูปแบบในการ build app รวมถึงแบ่งการทำงานข้างในแต่ละขั้นตอน หรือเรียกว่า task เพื่อแยกส่วนการทำงานต่าง ๆ ออกจากกัน

เดิมทีไฟล์ gradle ซึ่งไฟล์ที่ว่าคือ build.gradle มีหน้าที่ควบคุมการทำงานในส่วนต่าง ๆ ของโปรเจกต์ เป็นภาษา groovy

ไฟล์หลัก ๆ ที่เราใช้กัน

build.gradle ของ module

เช่น app/build.gradle

  • ประกาศ plugins ที่ใช้ในจัดเตรียม library และ infrastructure ที่จำเป็นในโปรเจกต์นั้น ๆ
// build app ได้
apply plugin: 'com.android.application' 
// ใช้ภาษา Kotlin ในโปรเจกต์เราได้
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
  • กำหนด Android property หรือค่าต่าง ๆ ที่กำหนดลงโปรเจกต์แอพของเรา เพื่อทำให้สามารถทำงานต่าง ๆ บน Android ได้
android {
   compileSdkVersion 34
   buildToolsVersion "34.0.0"
   defaultConfig {
       applicationId "com.example.sample"
       minSdkVersion 23
       targetSdkVersion 34
  }
}
  • จัดการ dependency ต่าง ๆ โดยการประกาศเรียกใช้งาน library
dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 
  implementation 'androidx.core:core-ktx:1.3.2'
  implementation 'androidx.appcompat:appcompat:1.2.0'
  implementation 'com.google.android.material:material:1.2.1'
  ...
}

build.gradle ของโปรเจกต์

  • เชื่อมต่อกับ repository ซึ่งเป็นที่เก็บ library ต่าง ๆเพื่อ download code ลงมาเก็บลงบนเครื่อง
repositories {
    google()
    mavenCentral()
}

setting.gradle

เป็นไฟล์ที่บอกว่าโปรเจกต์นี้เราจะใช้ module ใดบ้าง ปกติจะแค่นี้เนอะ

include ':app'

ทำไมต้องเปลี่ยนมาเป็น Kotlin DSL

เนื่องจาก Android Gradle plugin 4.0 นั้น support ภาษา Kotlin ที่เรารัก ซึ่งใช้แทนภาษา groovy เพราะว่าพอเป็น Kotlin แล้วมันอ่านง่ายกว่า พวกกับได้ compile-time checking ที่ดีขึ้น รวมถึง IDE ก็ support ด้วย

ตั้งแต่ Android Studio Giraffe โปรเจกต์ใหม่จะถูกบังคับให้มาใช้ แบบ Kotlin DSL แทน เป็น build.gradle.kts ซึ่งถ้าโปรเจกต์ที่เรามีอยู่จะย้ายตาม ต้องมีการปรับบางส่วนเพื่อให้ support ตัว Kotlin DSL นี้

ทำความรู้จัก syntax ของ Kotlin DSL แบบคร่าว ๆ

เริ่มกันที่ syntax ก่อน เอาแบบคร่าว ๆ

  • ก่อนอื่น ไฟล์เพิ่ม .kts ต่อท้าย จะมี build.gradle.kts และ setting.gradle.kts
  • เพิ่ม parent ให้กับ method ที่เรียก โดยมีวงเล็บครอบเหมือนเรียก function
// build.gradle
compileSdkVersion 30

// build.gradle.kts
compileSdkVersion(30)
  • ใช้ = ในการ assign ค่า
// build.gradle
java {
    sourceCompatibility JavaVersion.VERSION_17
    targetCompatibility JavaVersion.VERSION_17
}

// build.gradle.kts
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}
  • ใช้ " ในการ quote string และใช้คุณสมบัติของ Kotlin อย่าง $ ในการเรียกค่าจากตัวแปรนั้น ๆ
// build.gradle
myRootDirectory = "$project.rootDir/tools/proguard-rules-debug.pro"  

// build.gradle.kts
myRootDirectory = "${project.rootDir}/tools/proguard-rules-debug.pro"
  • เดิมใช้ def ตอนนี้ใช้ var กับ val ในการประกาศตัวแปร
// build.gradle
myRootDirectory = "$project.rootDir/tools/proguard-rules-debug.pro"  

// build.gradle.kts
myRootDirectory = "${project.rootDir}/tools/proguard-rules-debug.pro"
  • ตัวแปรที่เป็น boolean properties มี is นำหน้า
// build.gradle
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            ...
        }
        debug {
            debuggable true
            ...
        }
    ... 

// build.gradle.kts
android {
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            isShrinkResources = true
            ...
        }
        getByName("debug") {
            isDebuggable = true
            ...
        }
    ...
  • ใช้ list กับ map
// build.gradle
jvmOptions += ["-Xms4000m", "-Xmx4000m", "-XX:+HeapDumpOnOutOfMemoryError</code>"]

// build.gradle.kts
jvmOptions += listOf("-Xms4000m", "-Xmx4000m", "-XX:+HeapDumpOnOutOfMemoryError")

// build.gradle
def myMap = [key1: 'value1', key2: 'value2']

// build.gradle.kts
val myMap = mapOf("key1" to "value1", "key2" to "value2")

มาลองกับไฟล์จริงกัน

หลังจากทำความเข้าใจกันสักพัก มาที่ไฟล์จริงกันบ้าง

พวกที่เชื่อมต่อกับ repository ย้ายไป setting.gradle.kts ส่วนที่เรียก module ต่าง ๆ ยังเหมือนเดิม

// setting.gragle.kts
pluginManagement {
    repositories {
        google {
        mavenCentral()
        gradlePluginPortal()
    }
}

include(":app")

ในส่วน build.gradle.kts ของโปรเจกต์ จะมีแค่ plugin ที่ใช้ในโปรเจกต์

ถ้าอันไหนไม่อยากใช้ที่ root project ให้ใส่ apply false ไป

// build.gragle.kts (project)
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 มี set plugin ที่ล้อกับโปรเจกต์

// build.gragle.kts (project)
plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   ...
}

พวกกำหนด Android property และ dependency ยังคงเหมือนเดิม แค่เปลี่ยน syntax เฉย ๆ นะ

แล้ว toml คืออะไร?

ฉันก็ไม่รู้เหมือนกัน55555555 หยอก ๆ

toml ย่อมาจาก Tom's Obvious, Minimal Language เป็นรูปแบบไฟล์ที่ใช้สำหรับการจัดเก็บข้อมูลโครงสร้าง เหมาะอย่างยิ่งสำหรับการกำหนดค่าและการจัดการ dependencies โดยออกแบบมาให้อ่านง่าย เขียนง่าย ยืดหยุ่น และมีประสิทธิภาพ จึงเหมาะกับการจัดการ dependency และการตั้งค่า config ต่าง ๆ ในโปรเจกต์

สำหรับการนำมาใช้ในโปรเจกต์ Android จะเป็นไฟล์ชื่อว่า libs.version.toml ข้างในไฟล์จะแบ่งเป็น 3 ส่วน แบบนี้

[versions]
...

[libraries]
...

[plugins]
...
  • version: เก็บเลข version ทั้งของ library และ plugin ไว้ที่นี่
  • libraries: ใส่ dependency ที่เราใช้
  • plugins: ใส่ plugin ที่เราใช้ในโปรเจกต์

นำ Kotlin DSL และ toml มาประกอบร่าง

และนี่คือสิ่งที่เห็นในโปรเจกต์รวมมิจ ที่มีทั้ง Kotlin DSL และ toml

แล้วเราจะเพิ่มตัว open source notice เข้าไปยังไงนะ

เริ่มต้นที่ libs.version.toml เลย เราต้องใส่ตัว oss ทั้งที่ plugin และ library เลย หน้าตาจะเป็นแบบนี้

[versions]
...
ossLicensesPlugin = "0.10.6"
oss = "17.0.1"

[libraries]
...
play-oss-licenses = { module = "com.google.android.gms:play-services-oss-licenses", version.ref = "oss" }

[plugins]
...
ossLicenses = { id = "com.google.android.gms.oss-licenses-plugin", version.ref = "ossLicensesPlugin" }
  • version: ใส่เลข version ของตัว oss โดยของ plugin คือ 0.10.6 และ library คือ 17.0.1
  • libraries: dependency เป็น com.google.android.gms:play-services-oss-licenses:17.0.0 ดังนั้นเราจะแยกเป็น module ของ dependency เป็น com.google.android.gms:play-services-oss-licenses และ version เป็น oss
  • plugins: ตัว classpath มันเป็น classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' และเรียกใช้ plugin เป็น com.google.android.gms.oss-licenses-plugin ซึ่ง syntax ที่ถูกต้องเป็นตัวที่ใช้ใน plugin มี module เป็น com.google.android.gms.oss-licenses-plugin และ version เป็น ossLicensesPlugin จุดนี้ที่เกิดปัญหา มาอ่านกันต่อ

จากนั้นเอาสิ่งที่เรา set จาก toml ไปใช้ต่อที่แรก คือ build.gradle.kts ของ module เพื่อเรียกใช้ dependency กัน

dependencies {
    ...
    implementation(libs.play.oss.licenses)
}

และไปใช้ plugin ที่ build.gradle.kts ของโปรเจกต์

dependencies {
    ...
    alias(libs.plugins.ossLicenses) apply false
}

แต่ของ plugin จะเกิดปัญหา error นี้ขึ้นมา

พอเราไม่เรียกใช้ plugin ของ oss ทำให้พอเราเรียกเปิดหน้า open source notice มาทำงานไม่ถูกต้อง

วิธีแก้เราหานานมากกกกกกก จนมาเจอวิธีจาก stackoverflow ว่าให้ไป config resolutionStrategy ที่ settings.gradle.kts แบบนี้

pluginManagement {
    repositories {
        google()
    }
    // อันที่เพิ่ม
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "com.google.android.gms.oss-licenses-plugin") {
                useModule("com.google.android.gms:oss-licenses-plugin:${requested.version}")
            }
        }
    }
}
Fully support standard Gradle plugins block · Issue #223 · google/play-services-plugins
Describe the bug It appears that it is still necessary to use the antiquated style of loading oss-licenses-plugin via a buildscript classpath definition in the project root build.gradle, instead of...

พอแก้ปุ๊ปหายดีเรียบร้อย ทำงานได้ถูกต้อง เย้ ๆ

ทั้งหมดที่เราเรียนรู้ก็จะประมาณนี้แหละทุกคน คิดว่าน่าจะเป็นประโยชน์กับคนที่เจอ setting ใหม่แบบนี้ แล้วตกอกตกใจเช่นเรา

Reference

สร้างหน้า Open Source Notices บน Android Application
พอดีมีเหตุที่แอพทั้งออฟฟิศต้องทำ แล้วก็สงสัยด้วยแหละว่าจริงๆต้องทำยังไง ต้องทำเองไหม ต้องเอา license มาจากไหน บลาๆ
Migrate your build configuration from Groovy to Kotlin | Android Studio | Android Developers
Migrate your Gradle configuration files from Groovy to Kotlin.
Configure your build | Android Studio | Android Developers
The Android build system compiles app resources and source code and packages them into APKs that you can test, deploy, sign, and distribute.
มาเปลี่ยน Gradle ของเราให้ใช้ Kotlin แทน Groovy กันดีกว่า - Migration
เพราะปัญหาสำหรับนักพัฒนาแอนดรอยด์ที่ยังใช้ Groovy อยู่ เพราะต้องทำงานกับโปรเจคเก่าที่เป็น Groovy ทำให้เวลาย้ายไปใช้ Kotlin ก็จะต้องแก้โค้ดเก่าที่มีอยู่ด้วย
TOML: Tom’s Obvious Minimal Language
Add build dependencies | Android Studio | Android Developers
Learn how to add build dependencies using the Gradle build system in Android Studio.

ติดตามข่าวสารตามช่องทางต่าง ๆ และทุกช่องทางโดเนทกันไว้ที่นี่เลย แนะนำให้ใช้ tipme เน้อ ผ่าน promptpay ได้เต็มไม่หักจ้า

ติดตามข่าวสารแบบไว ๆ มาที่ Twitter เลย บางอย่างไม่มีในบล็อก และหน้าเพจนะ

Tags

Minseo Chayabanjonglerd

I am a full-time Android Developer and part-time contributor with developer community and web3 world, who believe people have hard skills and soft skills to up-skill to da moon.