Menggunakan pola Kotlin umum dengan Android

Topik ini berfokus pada beberapa aspek bahasa Kotlin yang paling berguna saat melakukan pengembangan untuk Android.

Menangani fragmen

Bagian berikut menggunakan contoh Fragment untuk menyoroti beberapa fitur terbaik Kotlin.

Pewarisan

Anda dapat mendeklarasikan class di Kotlin dengan kata kunci class. Pada contoh berikut, LoginFragment adalah subclass dari Fragment. Anda dapat menunjukkan pewarisan menggunakan operator : antara subclass dan induknya:

class LoginFragment : Fragment()

Pada deklarasi class ini, LoginFragment bertanggung jawab memanggil konstruktor superclass-nya, Fragment.

Dalam LoginFragment, Anda dapat mengganti sejumlah callback siklus proses untuk menanggapi perubahan status pada Fragment Anda. Untuk mengganti fungsi, gunakan kata kunci override, seperti yang ditunjukkan pada contoh berikut:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

Untuk mereferensikan fungsi di class induk, gunakan kata kunci super, seperti yang ditunjukkan pada contoh berikut:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

Nullability dan inisialisasi

Pada contoh sebelumnya, beberapa parameter dalam metode yang diganti memiliki jenis yang diberi akhiran dengan tanda tanya ?. Hal tersebut menunjukkan bahwa argumen yang diteruskan untuk parameter ini bisa berupa null. Pastikan untuk menangani nullability dengan aman.

Di Kotlin, Anda harus menginisialisasi properti objek saat mendeklarasikan objek. Ini berarti bahwa jika Anda memperoleh instance dari suatu class, Anda bisa langsung mereferensikan salah satu properti yang dapat diaksesnya. Namun, objek View di dalam Fragment belum siap untuk di-inflate hingga memanggil Fragment#onCreateView, sehingga Anda perlu sebuah cara untuk mengalihkan inisialisasi properti untuk View.

lateinit memungkinkan Anda mengalihkan inisialisasi properti. Saat menggunakan lateinit, Anda harus melakukan inisialisasi properti secepatnya.

Contoh berikut menunjukkan penggunaan lateinit untuk menetapkan objek View dalam onViewCreated:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

Konversi SAM

Anda dapat memproses peristiwa klik di Android dengan mengimplementasikan antarmuka OnClickListener. Objek Button berisi fungsi setOnClickListener() yang menerima implementasi dari OnClickListener.

OnClickListener memiliki metode abstrak tunggal, onClick(), yang harus Anda implementasikan. Karena setOnClickListener() selalu mengambil OnClickListener sebagai argumen, dan OnClickListener selalu memiliki metode abstrak tunggal yang sama, implementasi ini dapat direpresentasikan menggunakan fungsi anonim di Kotlin. Proses ini dikenal sebagai konversi Metode Abstrak Tunggal, atau konversi SAM.

Konversi SAM dapat membuat kode Anda jauh lebih rapi. Contoh berikut menunjukkan cara menggunakan konversi SAM guna mengimplementasikan OnClickListener untuk Button:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

Kode dalam fungsi anonim yang diteruskan ke setOnClickListener() akan dijalankan ketika pengguna mengklik loginButton.

Objek pendamping

Objek pendamping menyediakan mekanisme untuk menentukan variabel atau fungsi yang ditautkan secara konseptual ke suatu jenis, tetapi tidak terikat ke objek tertentu. Objek pendamping mirip dengan menggunakan kata kunci static Java untuk variabel dan metode.

Pada contoh berikut, TAG adalah konstanta String. Anda tidak perlu instance unik dari String untuk setiap instance LoginFragment, sehingga Anda harus menentukannya pada objek pendamping:

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

Anda dapat menentukan TAG di tingkat atas file, tetapi file tersebut juga mungkin memiliki sejumlah besar variabel, fungsi, dan class yang juga ditentukan di tingkat atas. Objek pendamping membantu menghubungkan variabel, fungsi, dan definisi class tanpa merujuk pada instance tertentu dari class tersebut.

Delegasi properti

Saat menginisialisasi properti, Anda mungkin mengulangi beberapa pola Android yang lebih umum, seperti mengakses ViewModel dalam Fragment. Guna menghindari kode duplikat berlebih, Anda bisa menggunakan sintaksis delegasi properti Kotlin.

private val viewModel: LoginViewModel by viewModels()

Delegasi properti menyediakan implementasi umum yang dapat Anda gunakan kembali di seluruh aplikasi. Android KTX menyediakan beberapa delegasi properti untuk Anda. Misalnya, viewModels mengambil ViewModel yang dicakupkan ke Fragment saat ini.

Delegasi properti menggunakan refleksi, yang menambahkan beberapa overhead performa. Hasilnya adalah sintaks ringkas yang menghemat waktu pengembangan.

Nullability

Kotlin memberikan aturan nullability yang ketat yang menjaga keamanan jenis di seluruh aplikasi Anda. Di Kotlin, referensi ke objek tidak boleh berisi nilai null secara default. Untuk menetapkan nilai null ke variabel, Anda harus mendeklarasikan jenis variabel nullable dengan menambahkan ? ke bagian akhir jenis dasar.

Sebagai contoh, ekspresi berikut ini ilegal di Kotlin. name adalah jenis String dan tidak nullable:

val name: String = null

Guna mengizinkan nilai null, Anda harus menggunakan jenis String nullable, String?, seperti yang ditunjukkan pada contoh berikut:

val name: String? = null

Interoperabilitas

Aturan ketat Kotlin membuat kode Anda lebih aman dan ringkas. Aturan ini menurunkan kemungkinan memiliki NullPointerException yang akan menyebabkan aplikasi Anda tidak bekerja. Selain itu, aturan tersebut mengurangi jumlah pemeriksaan null yang perlu Anda buat dalam kode.

Biasanya, Anda juga harus memanggil kode non-Kotlin saat menulis aplikasi Android, karena sebagian besar Android API ditulis dalam bahasa pemrograman Java.

Nullability adalah area utama tempat Java dan Kotlin memiliki perilaku berbeda. Java sedikit kurang ketat dengan sintaksis nullability.

Sebagai contoh, class Account memiliki beberapa properti, termasuk properti String yang bernama name. Java tidak memiliki aturan Kotlin seputar nullability, melainkan bergantung pada anotasi nullability opsional untuk mendeklarasikan secara eksplisit apakah Anda dapat menetapkan nilai null atau tidak.

Karena framework Android pada dasarnya ditulis dalam Java, Anda mungkin mengalami skenario ini saat memanggil ke API tanpa anotasi nullability.

Jenis platform

Jika Anda menggunakan Kotlin untuk mereferensikan anggota name yang tidak dianotasi yang ditentukan dalam class Account Java, compiler tidak akan mengetahui apakah String dipetakan ke String atau String? di dalam Kotlin. Ambiguitas ini direpresentasikan melalui jenis platform, String!.

String! tidak memiliki arti khusus untuk compiler Kotlin. String! dapat merepresentasikan String atau String?, dan compiler ini memungkinkan Anda menetapkan nilai dari salah satu jenis tersebut. Ingat bahwa Anda berisiko memunculkan NullPointerException jika merepresentasikan jenis sebagai String dan menetapkan nilai null.

Untuk mengatasi masalah ini, sebaiknya gunakan anotasi nullability setiap kali Anda menulis kode di Java. Anotasi ini membantu developer Java dan Kotlin.

Misalnya, berikut adalah class Account seperti yang ditentukan di Java:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

Salah satu variabel anggota, accessId, dianotasi dengan @Nullable, yang menunjukkan bahwa variabel tersebut dapat menyimpan nilai null. Selanjutnya, Kotlin akan memperlakukan accessId sebagai String?.

Untuk menunjukkan bahwa variabel tidak boleh null, gunakan anotasi @NonNull:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

Dalam skenario ini, name dianggap String non-nullable di Kotlin.

Anotasi nullability disertakan di semua Android API baru dan di banyak Android API yang sudah ada. Banyak library Java telah menambahkan anotasi nullability untuk mendukung developer Kotlin dan Java dengan lebih baik.

Menangani nullability

Jika tidak yakin dengan jenis Java, Anda harus menganggapnya sebagai nullable. Misalnya, anggota name dari class Account tidak dianotasi, jadi Anda harus menganggapnya sebagai String nullable.

Jika ingin memotong name agar nilainya tidak mencantumkan spasi kosong di awal atau akhir, Anda dapat menggunakan fungsi trim Kotlin. Anda dapat memotong String? secara aman dengan beberapa cara yang berbeda. Salah satu caranya adalah menggunakan operator pernyataan bukan null, !!, seperti yang ditunjukkan pada contoh berikut:

val account = Account("name", "type")
val accountName = account.name!!.trim()

Operator !! memperlakukan semua yang ada di sisi kiri sebagai non-null, sehingga dalam hal ini, Anda akan memperlakukan name sebagai String non-null. Jika hasil ekspresi di sebelah kirinya null, aplikasi akan memunculkan NullPointerException. Operator ini cepat dan mudah, tetapi harus digunakan seperlunya, karena operator tersebut dapat memperkenalkan ulang instance NullPointerException ke dalam kode Anda.

Pilihan yang lebih aman adalah menggunakan operator panggilan aman, ?., seperti yang ditunjukkan pada contoh berikut:

val account = Account("name", "type")
val accountName = account.name?.trim()

Dengan menggunakan operator panggilan aman, jika name non-null, maka hasil dari name?.trim() adalah nilai nama tanpa spasi kosong di awal atau akhir. Jika name adalah null, maka hasil dari name?.trim() adalah null. Artinya, aplikasi Anda tidak dapat memunculkan NullPointerException saat menjalankan pernyataan ini.

Meskipun operator panggilan aman menyelamatkan Anda dari potensi NullPointerException, operator akan meneruskan nilai null ke pernyataan berikutnya. Sebagai gantinya, Anda dapat menangani kasus null langsung menggunakan operator Elvis (?:), seperti yang ditunjukkan pada contoh berikut:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

Jika hasil ekspresi di sisi kiri operator Elvis null, maka nilai di sisi kanan ditetapkan ke accountName. Teknik ini berguna untuk memberikan nilai default yang tidak akan bernilai null.

Anda juga dapat menggunakan operator Elvis untuk kembali dari fungsi awal, seperti yang ditunjukkan pada contoh berikut:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

Perubahan Android API

Android API menjadi semakin cocok untuk Kotlin. Banyak Android API yang paling umum, termasuk AppCompatActivity dan Fragment, berisi anotasi nullability, dan panggilan tertentu seperti Fragment#getContext memiliki lebih banyak alternatif yang cocok untuk Kotlin.

Misalnya, mengakses Context dari Fragment hampir selalu non-null, karena sebagian besar panggilan yang dibuat di Fragment terjadi saat Fragment dilampirkan ke Activity (subclass Context). Oleh karena itu, Fragment#getContext tidak selalu menampilkan nilai non-null, karena terdapat skenario di mana Fragment tidak dilampirkan ke Activity. Dengan demikian, jenis nilai yang ditampilkan Fragment#getContext nullable.

Karena Context yang ditampilkan dari Fragment#getContext adalah nullable (dan dianotasikan sebagai @Nullable), Anda harus memperlakukannya sebagai Context? dalam kode Kotlin Anda. Ini berarti menerapkan salah satu operator yang disebutkan sebelumnya untuk mengatasi nullability sebelum mengakses properti dan fungsinya. Untuk beberapa skenario tersebut, Android berisi API alternatif yang memberikan kemudahan ini. Misalnya, Fragment#requireContext menampilkan Context non-null dan memunculkan IllegalStateException jika dipanggil ketika Context akan menjadi null. Dengan cara ini, Anda dapat memperlakukan Context yang dihasilkan sebagai non-null tanpa perlu operator panggilan aman atau solusi.

Inisialisasi properti

Properti di Kotlin tidak diinisialisasi secara default. Properti tersebut harus diinisialisasi saat class yang menyertakannya diinisialisasi.

Anda dapat menginisialisasi properti dengan beragam cara. Contoh berikut menunjukkan cara menginisialisasi variabel index dengan menetapkannya nilai pada deklarasi class:

class LoginFragment : Fragment() {
    val index: Int = 12
}

Inisialisasi ini juga dapat ditentukan dalam blok penginisialisasi:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

Pada contoh di atas, index diinisialisasi saat LoginFragment dibuat.

Namun, Anda mungkin memiliki beberapa properti yang tidak dapat diinisialisasi selama pembuatan objek. Misalnya, Anda mungkin ingin mereferensikan View dari dalam Fragment, yang berarti tata letaknya harus di-inflate terlebih dahulu. Inflasi tidak terjadi jika Fragment dibuat. Sebaliknya, hal tersebut di-inflate saat memanggil Fragment#onCreateView.

Salah satu cara untuk mengatasi skenario ini adalah dengan mendeklarasikan tampilan sebagai nullable dan menginisialisasinya secepatnya, seperti yang ditunjukkan pada contoh berikut:

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

Meskipun berfungsi seperti yang diharapkan, Anda sekarang harus mengelola nullability dari View setiap kali Anda mereferensikannya. Solusi yang lebih baik adalah menggunakan lateinit untuk inisialisasi View, seperti yang ditunjukkan pada contoh berikut:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

Kata kunci lateinit memungkinkan Anda menghindari inisialisasi properti ketika objek dibuat. Jika properti Anda direferensikan sebelum diinisialisasi, Kotlin akan memunculkan UninitializedPropertyAccessException, jadi pastikan Anda menginisialisasi properti secepatnya.