Chapter penutup section ini merangkum praktik terbaik yang mempengaruhi keseharian tim pengembang Kotlin. Topik-topik yang telah dibahas di chapter-chapter sebelumnya — kotlinc, Gradle, dependencies, version catalog, Maven — semuanya menyatu dalam keputusan praktis: bagaimana menulis build.gradle.kts yang sehat, bagaimana mengonfigurasi CI, dan bagaimana menjaga build tetap cepat seiring proyek tumbuh. Pada chapter ini kita akan membahas lima praktik yang paling berdampak, beserta alasan dan konsekuensi masing-masing.

1. Pin Versi di Version Catalog #

Praktik pertama: kumpulkan seluruh deklarasi versi di gradle/libs.versions.toml. Hindari menulis string versi langsung di build.gradle.kts. Manfaatnya:

  • Versi konsisten lintas modul: bila satu modul memakai kotlinx-coroutines-core:1.8.1, modul lain tidak akan secara tidak sengaja memakai 1.7.3.
  • Update satu tempat: cukup ubah satu baris di [versions] untuk upgrade seluruh modul.
  • IDE autocomplete: typed accessor libs.kotlinx.coroutines.core memberi saran sambil mengetik.
toml gradle/libs.versions.toml
[versions]
kotlin = "2.0.21"
coroutines = "1.8.1"

[libraries]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

2. Pakai Gradle Wrapper #

Wrapper memastikan versi Gradle yang dipakai developer dan CI identik. Selalu commit file wrapper ke repository:

text
gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties

Upgrade wrapper dengan perintah:

bash
./gradlew wrapper --gradle-version 8.10

3. Build Bersih di CI #

CI harus menghasilkan build yang deterministik. Beberapa praktik:

  • Selalu mulai dari container Docker yang sama, dengan versi JDK dan Gradle yang dipin.
  • Jalankan ./gradlew clean build (atau ./gradlew build --no-daemon --no-build-cache) untuk memastikan build dari nol berhasil.
  • Set SOURCE_DATE_EPOCH agar timestamp file di JAR deterministik.
  • Untuk verifikasi tambahan, bandingkan hash JAR dengan build sebelumnya.
yaml CI config (GitHub Actions)
jobs:
  build:
    runs-on: ubuntu-22.04
    container: eclipse-temurin:21-jdk
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: ./gradlew clean build
        env:
          SOURCE_DATE_EPOCH: 1700000000

4. Cache Dependensi #

Build cache memotong waktu build secara signifikan. Tiga lapisan cache yang harus diaktifkan:

text gradle.properties
org.gradle.parallel=true                # build multi-modul paralel
org.gradle.caching=true                # build cache lokal
org.gradle.configuration-cache=true    # cache hasil configuration phase
org.gradle.jvmargs=-Xmx2g -XX:+UseZGC  # heap daemon cukup besar

Untuk CI, cache direktori ~/.gradle/caches/ antar run:

yaml CI cache config
- uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', 'gradle/libs.versions.toml') }}

5. Hindari Dynamic Version #

Dynamic version (+, latest.release, latest.snapshot) membuat build tidak reproducible. Setiap eksekusi dapat menghasilkan versi dependensi yang berbeda. Beberapa alasan mengapa ini berbahaya:

  • Build tidak reproducible: dua build pada hari berbeda dapat menghasilkan JAR berbeda.
  • Risiko break mendadak: versi baru library dengan breaking change menyebabkan build gagal tanpa perubahan kode dari kita.
  • Sulit untuk audit: tidak jelas versi mana yang sebenarnya dipakai saat produksi.
kotlin build.gradle.kts — Hindari ini
// HINDARI:
implementation("com.example:lib:1.+")
implementation("com.example:lib:latest.release")

// GANTI DENGAN:
implementation(libs.example.lib)  // dari version catalog

Demonstrasi Best Practices #

Kode berikut mencantumkan lima praktik utama sebagai ringkasan.

kotlin
fun main() {
    println("=== Best Practices Build Tools ===")
    println()
    println("1. Pin versi di version catalog")
    println("   - Hindari dynamic version: implementation(\"lib:1.+\")")
    println("   - Pakai libs.versions.toml untuk semua versi")
    println()
    println("2. Pakai Gradle Wrapper")
    println("   - Commit gradlew, gradlew.bat, gradle/wrapper/* ke repository")
    println("   - Versi Gradle konsisten di semua developer dan CI")
    println()
    println("3. Build bersih di CI")
    println("   - Tidak ada state lokal (sebelum build: ./gradlew clean)")
    println("   - Gunakan container deterministik (image Docker versi pin)")
    println("   - Set SOURCE_DATE_EPOCH untuk reproducibility")
    println()
    println("4. Cache dependensi")
    println("   - Aktifkan Gradle cache: gradle.properties -> org.gradle.caching=true")
    println("   - CI: cache direktori ~/.gradle/caches/")
    println("   - Build cache remote: Gradle Enterprise / Develocity")
    println()
    println("5. Hindari dynamic version (+)")
    println("   - '+' / 'latest.release' membuat build tidak reproducible")
    println("   - Risiko: versi baru library break compatibility tanpa peringatan")
    println("   - Pakai dependabot / renovate untuk update versi otomatis")
}

Output yang Dihasilkan:

text
=== Best Practices Build Tools ===

1. Pin versi di version catalog
   - Hindari dynamic version: implementation("lib:1.+")
   - Pakai libs.versions.toml untuk semua versi

2. Pakai Gradle Wrapper
   - Commit gradlew, gradlew.bat, gradle/wrapper/* ke repository
   - Versi Gradle konsisten di semua developer dan CI

3. Build bersih di CI
   - Tidak ada state lokal (sebelum build: ./gradlew clean)
   - Gunakan container deterministik (image Docker versi pin)
   - Set SOURCE_DATE_EPOCH untuk reproducibility

4. Cache dependensi
   - Aktifkan Gradle cache: gradle.properties -> org.gradle.caching=true
   - CI: cache direktori ~/.gradle/caches/
   - Build cache remote: Gradle Enterprise / Develocity

5. Hindari dynamic version (+)
   - '+' / 'latest.release' membuat build tidak reproducible
   - Risiko: versi baru library break compatibility tanpa peringatan
   - Pakai dependabot / renovate untuk update versi otomatis

Mari kita telaas kode di atas:

  1. Lima praktik: dipilih karena berdampak paling besar pada kualitas build. Praktik lain (pemilihan configuration, structure project, reproducibility) telah dibahas di chapter tersendiri.
  2. Dynamic version sebagai sumber utama masalah: bahaya utama adalah reproducibility dan auditabilitas. Dependabot memberi manfaat update otomatis tanpa risiko dynamic version.
  3. Build cache: tiga lapisan (local cache, configuration cache, daemon) bekerja sama. Aktifkan ketiganya untuk hasil optimal.

Praktik Tambahan #

Beberapa praktik yang juga penting tetapi tidak masuk lima besar:

  • Modularisasi: pisahkan kode ke modul-modul kecil agar build inkremental efektif.
  • Deteksi unused dependencies: jalankan ./gradlew dependencies dan dependencyInsight secara berkala untuk membersihkan.
  • Lock files (Gradle 7+): ./gradlew dependencies --write-verification-metadata sha-256 untuk verifikasi integritas dependensi.
  • SBOM (Software Bill of Materials): gunakan plugin seperti org.cyclonedx.bom untuk menghasilkan daftar dependensi lengkap untuk audit keamanan.

Referensi Lanjutan #

  • Gradle — panduan resmi Kotlin + Gradle sebagai dasar praktik terbaik build Kotlin
  • Kotlin with Maven — panduan resmi Kotlin + Maven untuk praktik terbaik proyek berbasis Maven
  • Coding conventions — panduan konvensi penulisan Kotlin yang juga relevan untuk tata nama build script

Rangkuman Section #

Section Build Tools dan Dependency Management berakhir di sini. Kita telah menelusuri kotlinc sebagai compiler dasar, Gradle sebagai build tool modern, struktur proyek, deklarasi dependensi, lifecycle build, version catalog, resolusi konflik, Maven sebagai alternatif, build variants, dan reproducible builds. Pemahaman utuh atas topik ini menjadi modal untuk membaca dan menulis build script yang sehat. Section berikutnya akan berfokus pada topik yang sama pentingnya untuk pengembangan profesional: debugging dan kualitas kode.