Ketika null Berubah Jadi Kata "null"
Hampir dua tahun terakhir saya mengerjakan satu aplikasi Android yang sudah lama jalan di produksi — basisnya Java, arsitekturnya MVP, dipakai ribuan orang setiap hari.
Salah satu pekerjaan yang terus berjalan di latar belakang: memigrasi kodenya ke Kotlin. Bukan proyek besar dengan tenggat, tapi cicilan — satu modul per beberapa minggu, di sela-sela fitur baru. Sampai sekarang masih separuh jalan, dan masih jalan.
Tulisan ini soal satu jenis bug yang berulang saya temui di tengahnya. Menurut saya ini bug paling khas untuk migrasi Java→Kotlin, karena justru muncul dari fitur Kotlin yang seharusnya melindungi kita.
Bentuk bug-nya
Bayangkan sebuah form yang mengirim data profil ke server. Setelah dimigrasi ke Kotlin, kodenya kira-kira seperti ini:
params["city_id"] = user.selectedCity?.id.toString()
params["region_id"] = user.selectedRegion?.id.toString()
Kelihatan aman. Ada ?., jadi kalau selectedCity belum dipilih tidak akan
crash.
Dan memang tidak crash. Yang terjadi lebih halus: server menerima string
"null" — empat huruf, n-u-l-l — lalu gagal mem-parse-nya sebagai angka.
Kenapa bisa begitu
Perhatikan di mana ?. berhenti bekerja:
user.selectedCity?.id.toString()
// ^^^ ^^^
// aman TIDAK aman
?. hanya melindungi pemanggilan .id. Kalau selectedCity null, ekspresi
selectedCity?.id menghasilkan null — dan .toString() berikutnya dipanggil
pada hasil null itu, bukan di-skip.
Di Kotlin, null.toString() bukan error. Itu memanggil ekstensi
Any?.toString(), yang dengan patuh mengembalikan string "null".
Jadi alih-alih crash yang langsung kelihatan, kita dapat data kotor yang lolos sampai ke server.
Perbaikannya satu operator, plus satu pemanggilan:
params["city_id"] = user.selectedCity?.id?.toString().orEmpty()
// ^
// ?. kedua, dan .orEmpty() di ujung
Sekarang null jadi string kosong, bukan kata “null”.
Kenapa ini menarik
Bug seperti ini tidak akan muncul di Java.
Di Java, user.getSelectedCity().getId() pada objek null langsung melempar
NullPointerException. Berisik, tapi jujur — kamu tahu persis ada yang salah, dan
tahu di baris mana.
Kotlin menawarkan ?. supaya kita tidak perlu menulis pengecekan null bertingkat.
Tapi kalau operatornya dipakai setengah jalan, hasilnya bukan keamanan — hanya
kegagalan yang lebih sunyi. Crash berubah jadi data rusak, dan data rusak jauh
lebih mahal untuk ditemukan.
Yang paling merepotkan: bug ini tidak muncul di jalur normal. Selama pengguna mengisi semua field, semuanya baik-baik saja. Baru terlihat ketika ada field opsional yang dilewati — dan itu justru kasus yang paling jarang dites.
Dua hal yang saya bawa dari migrasi ini
Null-safety itu properti batas sistem, bukan properti bahasa. Kotlin menjaga
apa yang terjadi di dalam kodenya. Yang keluar lewat jaringan cuma string — dan di
situ "null", "", dan tidak-mengirim-apa-apa adalah tiga hal yang sangat
berbeda bagi server. Compiler tidak bisa menolongmu di perbatasan itu.
Migrasi bertahap itu benar, tapi jangan mekanis. Setiap modul yang saya
pindahkan, saya baca ulang alur datanya, bukan cuma menerjemahkan sintaksnya. Bug
di atas justru lolos kalau saya hanya mengubah getId() jadi ?.id lalu
menganggap pekerjaan selesai.
Dan satu keputusan yang saya syukuri: arsitekturnya tidak saya ganti. MVP tetap MVP, hanya bahasanya yang berpindah. Menggabungkan dua perubahan besar sekaligus di aplikasi yang dipakai orang setiap hari akan membuat setiap bug sulit dilacak — salah bahasa, atau salah arsitektur? Satu perubahan pada satu waktu.