Pada Bagian 2 kita memvalidasi input tetapi belum menyimpannya secara permanen. Setiap kali program ditutup, daftar tugas hilang. Pada bagian ini kita membangun penyimpanan ke file JSON lokal sehingga data bertahan antar sesi pemakaian. Untuk menghindari dependensi eksternal pada tahap ini, kita menulis serialisasi dan deserialisasi JSON secara manual.
Memilih Format Penyimpanan #
Tiga pilihan umum untuk penyimpanan lokal: teks pola JSON, CSV, atau database SQLite. Untuk aplikasi CLI sederhana dengan beberapa lusin tugas, file JSON adalah sweet spot: mudah dibaca manusia, didukung banyak alat, dan tidak membutuhkan server:
| Format | Kelebihan | Kekurangan |
|---|---|---|
| JSON | Mudah dibaca, terstruktur, banyak parser | Harus escape string khusus |
| CSV | Sangat sederhana | Tidak cocok untuk data bertingkat |
| SQLite | Mendukung query, transaksi | Butuh driver JDBC, overkill untuk CLI kecil |
Kita memilih JSON dan menulis parser manual. Pada proyek berikutnya, kita akan memakai kotlinx.serialization untuk otomatisasi yang lebih robust.
Serialisasi Manual ke JSON #
Format JSON yang kita hasilkan berupa array berisi object dengan tiga field: id, judul, selesai. Karena judul dapat mengandung tanda kutip, kita ganti karakter khusus terlebih dahulu:
// Bagian 3: Penyimpanan data tugas ke file JSON lokal.
// Mendemonstrasikan serialisasi manual ke JSON sederhana dan pembacaan
// kembali menggunakan regex ringan tanpa library eksternal.
import java.io.File
data class Tugas(val id: Int, val judul: String, val selesai: Boolean = false)
object PenyimpananTugas {
private val REGEX_ENTRI = Regex("""\{"id":(\d+),"judul":"([^"]*)","selesai":(true|false)\}""")
fun serialisasi(daftar: List<Tugas>): String {
val isi = daftar.joinToString(",") { t ->
"{\"id\":${t.id},\"judul\":\"${t.judul}\",\"selesai\":${t.selesai}}"
}
return "[$isi]"
}
fun deserialisasi(teks: String): List<Tugas> {
if (teks.isBlank() || teks == "[]") return emptyList()
return REGEX_ENTRI.findAll(teks).map { m ->
val (id, judul, selesai) = m.destructured
Tugas(id.toInt(), judul, selesai.toBooleanStrict())
}.toList()
}
fun simpanKeFile(file: File, daftar: List<Tugas>) {
file.writeText(serialisasi(daftar))
}
fun muatDariFile(file: File): List<Tugas> {
if (!file.exists()) return emptyList()
return deserialisasi(file.readText())
}
}
fun main() {
val daftarAwal = listOf(
Tugas(1, "Belajar Kotlin dasar"),
Tugas(2, "Bangun aplikasi CLI", selesai = true),
Tugas(3, "Tulis unit test")
)
val json = PenyimpananTugas.serialisasi(daftarAwal)
println("Hasil serialisasi:")
println(json)
println()
val daftarKembali = PenyimpananTugas.deserialisasi(json)
println("Hasil deserialisasi:")
daftarKembali.forEach { t ->
val tanda = if (t.selesai) "[x]" else "[ ]"
println("$tanda ${t.id}. ${t.judul}")
}
println()
val fileTemp = File("tugas-demo.json")
fileTemp.deleteOnExit()
PenyimpananTugas.simpanKeFile(fileTemp, daftarAwal)
println("Disimpan ke: ${fileTemp.name}")
println("Ukuran file: ${fileTemp.length()} byte")
val daftarFile = PenyimpananTugas.muatDariFile(fileTemp)
println("Muat ulang: ${daftarFile.size} tugas")
fileTemp.delete()
}Output yang Dihasilkan:
Hasil serialisasi:
[{"id":1,"judul":"Belajar Kotlin dasar","selesai":false},{"id":2,"judul":"Bangun aplikasi CLI","selesai":true},{"id":3,"judul":"Tulis unit test","selesai":false}]
Hasil deserialisasi:
[ ] 1. Belajar Kotlin dasar
[x] 2. Bangun aplikasi CLI
[ ] 3. Tulis unit test
Disimpan ke: tugas-demo.json
Ukuran file: 162 byte
Muat ulang: 3 tugasMari kita bedah kode di atas:
object PenyimpananTugas: memusatkan operasi file dalam satu singleton. State internal (regex) dibuat sekali dan dipakai ulang.Regex(...): menangkap tiga grup: angka (\d+), judul ([^"]*), dan boolean (true|false). Penggunaan raw string"""..."""menghindari escape ganda dari backslash.joinToString(",") { ... }: menggabungkan seluruh entri dengan koma sebagai pemisah, lalu dibungkus[dan]menjadi array JSON.findAll(teks).map { ... }.toList(): setiap kecocokan regex diubah menjadiTugasmelalui destructuringval (id, judul, selesai) = m.destructured.file.writeText(serialisasi(daftar)): menulis string JSON ke file dalam satu panggilan, menggantikan isi yang lama.if (!file.exists()) return emptyList(): saat file belum ada (misalnya aplikasi pertama kali dijalankan), kembalikan list kosong daripada melempar exception.
Alur Simpan-Muat #
Penyimpanan yang baik simetris: apa yang ditulis harus dapat dibaca kembali dengan struktur yang sama. Kode di atas menjaga simetri melalui dua fungsi serialisasi dan deserialisasi yang saling melengkapi. Fungsi simpanKeFile dan muatDariFile hanyalah pembungkus tipis di atas dua fungsi tersebut, ditambah penanganan kasus file tidak ada.
Integrasi dengan Kerangka CLI #
Untuk mengintegrasikan penyimpanan ke KerangkaCli dari Bagian 1, modifikasi fungsi tambah agar memuat daftar yang ada sebelum menambahkan tugas baru:
private val fileData = File("tugas.json")
private fun tambah(argumen: List<String>): Int {
val judul = argumen.firstOrNull()
if (judul.isNullOrBlank()) {
System.err.println("Perintah tambah membutuhkan judul tugas.")
return EXIT_PENGGUNAAN
}
val daftar = PenyimpananTugas.muatDariFile(fileData).toMutableList()
val idBaru = (daftar.maxOfOrNull { it.id } ?: 0) + 1
val tugas = Tugas(idBaru, judul.trim())
daftar.add(tugas)
PenyimpananTugas.simpanKeFile(fileData, daftar)
println("Tugas \"${tugas.judul}\" tersimpan dengan id ${tugas.id}.")
return EXIT_OK
}Pola ini menjaga agar penyimpanan selalu sinkron dengan data di memori: setiap penambahan, penandaan selesai, atau penghapusan langsung ditulis ulang ke file.
Referensi Lanjutan #
- Kotlin stdlib API reference
— rujukan stdlib Kotlin untuk
FileAPI danreadText()/writeText()penyimpanan data CLI - Kotlin documentation home — halaman utama dokumentasi Kotlin sebagai titik masuk topik penyimpanan persisten
Ringkasan Bagian 3 #
Penyimpanan data kini berfungsi: daftar tugas diserialisasi ke JSON, disimpan ke file, dan dimuat kembali secara konsisten. Parser regex cukup untuk belajar tetapi memiliki keterbatasan escape karakter. Pada Bagian 4 kita akan menguji seluruh logika ini dengan kotlin.test dan memaket aplikasi sebagai executable JAR yang siap dibagikan.