Paradigma Compose

Jetpack Compose adalah Toolkit UI deklaratif yang modern untuk Android. Compose memudahkan Anda menulis dan mengelola UI aplikasi dengan menyediakan API deklaratif yang memungkinkan Anda merender UI aplikasi tanpa harus mengubah tampilan frontend. Istilah ini memerlukan penjelasan, tetapi implikasinya penting bagi desain aplikasi Anda.

Paradigma pemrograman deklaratif

Secara historis, hierarki tampilan Android telah dianggap sebagai hierarki widget UI. Ketika status aplikasi berubah karena hal-hal seperti interaksi pengguna, hierarki UI perlu diupdate untuk menampilkan data saat ini. Cara paling umum untuk mengupdate UI adalah dengan menjalankan hierarki menggunakan fungsi seperti findViewById(), dan mengubah node dengan memanggil metode seperti button.setText(String), container.addChild(View), atau img.setImageBitmap(Bitmap). Metode ini mengubah status internal widget.

Memanipulasi tampilan secara manual akan meningkatkan kemungkinan terjadinya error. Jika sepotong data dirender di beberapa tempat, Anda mungkin akan lupa mengupdate salah satu tampilan yang menampilkannya. Status ilegal juga mudah muncul jika ada dua update yang bentrok secara tidak terduga. Misalnya, update mungkin mencoba menetapkan nilai node yang baru saja dihapus dari UI. Secara umum, kompleksitas pemeliharaan software akan bertambah seiring banyaknya jumlah tampilan yang memerlukan update.

Selama beberapa tahun terakhir, seluruh industri telah mulai beralih ke model UI deklaratif yang sangat menyederhanakan teknik yang terkait dengan pembuatan dan pembaruan antarmuka pengguna. Teknik ini bekerja dengan membuat ulang seluruh layar secara konseptual dari awal, lalu hanya menerapkan perubahan yang diperlukan. Pendekatan ini menghindarkan kerumitan dalam mengupdate hierarki tampilan status saat ini secara manual. Compose adalah framework UI deklaratif.

Salah satu tantangan dalam membuat ulang seluruh layar adalah kemungkinan akan menjadi mahal, dalam hal waktu, daya komputasi, dan penggunaan baterai. Untuk mengurangi biaya ini, dengan cerdas Compose memilih bagian UI mana yang perlu digambar ulang pada waktu tertentu. Hal ini memang memiliki beberapa implikasi terhadap cara Anda mendesain komponen UI, seperti dibahas dalam Rekomposisi.

Fungsi composable sederhana

Dengan Compose, Anda dapat membuat antarmuka pengguna dengan cara menentukan kumpulan fungsi yang dapat dikomposisi, yang mengambil data dan membuat elemen UI. Contoh sederhananya adalah widget Greeting, yang mengambil String dan membuat widget Text yang menampilkan pesan sapaan.

Screenshot ponsel yang menampilkan teks "Hello World", dan kode untuk
fungsi Composable sederhana yang membuat
UI tersebut

Gambar 1. Fungsi sederhana yang dapat dikomposisi, yang meneruskan data dan menggunakannya untuk merender widget teks di layar.

Beberapa hal penting tentang fungsi ini:

  • Fungsi ini dianotasi dengan anotasi @Composable. Semua fungsi yang dapat dikomposisi harus memiliki anotasi ini; anotasi ini menginformasikan compiler Compose bahwa fungsi ini ditujukan untuk mengonversi data menjadi UI.

  • Fungsi ini mengambil data. Fungsi yang dapat dikomposisi dapat menerima parameter, yang memungkinkan logika aplikasi untuk menjelaskan UI. Dalam hal ini, widget kami menerima String sehingga dapat menyambut pengguna dengan nama mereka.

  • Fungsi ini menampilkan teks di UI. Hal ini dilakukan dengan memanggil fungsi composable Text(), yang benar-benar membuat elemen UI teks. Fungsi composable menampilkan hierarki UI dengan cara memanggil fungsi composable lainnya.

  • Fungsi ini tidak menampilkan apa pun. Fungsi Compose yang membuat UI tidak akan menampilkan apa pun, karena fungsi itu mendeskripsikan status layar yang diinginkan, bukan membuat widget UI.

  • Fungsi ini sangat cepat, idempoten, dan bebas efek samping.

    • Fungsi berperilaku dengan cara yang sama saat dipanggil beberapa kali dengan argumen yang sama, dan tidak menggunakan nilai lain seperti variabel global atau panggilan ke random().
    • Fungsi ini menjelaskan UI tanpa efek samping, seperti mengubah properti atau variabel global.

    Secara umum, semua fungsi composable harus ditulis dengan properti ini, untuk alasan yang dibahas dalam Rekomposisi.

Perubahan paradigma deklaratif

Dengan banyak toolkit UI berorientasi objek yang penting, Anda menginisialisasi UI dengan membuat instance hierarki widget. Anda sering melakukannya dengan meng-inflate file tata letak XML. Setiap widget mempertahankan status internal masing-masing, dan memperlihatkan metode pengambil dan penyetel yang memungkinkan logika aplikasi untuk berinteraksi dengan widget.

Dalam pendekatan deklaratif Compose, widget relatif stateless dan tidak menampilkan fungsi penyetel atau pengambil. Bahkan, widget tidak ditampilkan sebagai objek. Anda mengupdate UI dengan memanggil fungsi composable yang sama dengan argumen yang berbeda. Hal ini memudahkan untuk memberikan status ke pola arsitektur seperti ViewModel, seperti dijelaskan dalam Panduan untuk arsitektur aplikasi. Kemudian, komposisi Anda bertanggung jawab untuk mengubah status aplikasi saat ini menjadi UI setiap kali update data yang dapat diobservasi tersedia.

Ilustrasi aliran data di UI Compose, dari objek tingkat tinggi hingga
turunan
mereka.

Gambar 2. Logika aplikasi memberikan data ke fungsi tingkat atas yang dapat dikomposisi. Fungsi tersebut menggunakan data untuk mendeskripsikan UI dengan memanggil komposisi lain, dan meneruskan data yang sesuai ke komposisi-komposisi tersebut, dan ke arah bawah hierarki.

Saat pengguna berinteraksi dengan UI, UI memunculkan peristiwa seperti onClick. Peristiwa tersebut akan memberi tahu logika aplikasi, yang kemudian dapat mengubah status aplikasi. Saat status berubah, fungsi yang dapat dikomposisi dipanggil lagi dengan data baru. Hal ini menyebabkan elemen UI digambar ulang--proses ini disebut rekomposisi.

Ilustrasi tentang cara elemen UI merespons interaksi, dengan memicu peristiwa
yang ditangani oleh logika
aplikasi.

Gambar 3. Pengguna berinteraksi dengan elemen UI, menyebabkan peristiwa dipicu. Logika aplikasi merespons peristiwa, lalu fungsi yang dapat dikomposisi dipanggil lagi secara otomatis dengan parameter baru, jika perlu.

Konten dinamis

Karena fungsi composable bisa ditulis di Kotlin, bukan XML, fungsi tersebut bisa menjadi sama dinamisnya dengan kode Kotlin lainnya. Misalnya, Anda ingin membuild UI yang menyapa daftar pengguna:

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}

Fungsi ini mengambil daftar nama dan membuat sapaan untuk setiap pengguna. Fungsi yang dapat dikomposisi terkadang sangat canggih. Anda dapat menggunakan pernyataan if untuk memutuskan apakah Anda ingin menampilkan elemen UI tertentu. Anda dapat menggunakan loop. Anda dapat memanggil fungsi pembantu. Anda memiliki fleksibilitas penuh untuk bahasa utama. Kekuatan dan fleksibilitas ini adalah salah satu keunggulan utama Jetpack Compose.

Rekomposisi

Dalam model UI imperatif, untuk mengubah widget, Anda memanggil penyetel pada widget untuk mengubah status internalnya. Di Compose, Anda memanggil lagi fungsi yang dapat dikomposisi dengan data baru. Jika hal ini dilakukan, fungsi akan direkomposisi--widget yang dibuat oleh fungsi akan digambar ulang, jika perlu, dengan data baru. Framework Compose dapat merekomposisi secara cerdas komponen-komponen yang berubah saja.

Misalnya, perhatikan fungsi yang dapat dikomposisi ini, yang menampilkan tombol:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}

Setiap kali tombol diklik, pemanggil akan mengupdate nilai clicks. Compose akan memanggil ulang lambda dengan fungsi Text untuk menampilkan nilai baru; proses ini disebut rekomposisi. Fungsi lain yang tidak bergantung pada nilai tidak akan direkomposisi.

Seperti yang telah kita diskusikan, menyusun ulang seluruh hierarki UI bisa menjadi komputasi yang mahal, yang menggunakan daya komputasi dan masa pakai baterai. Compose memecahkan masalah ini dengan rekomposisi cerdas.

Rekomposisi adalah proses memanggil lagi fungsi yang dapat dikomposisi saat input berubah. Hal ini terjadi saat input fungsi berubah. Ketika Compose merekomposisi berdasarkan input baru, Compose hanya memanggil fungsi atau lambda yang mungkin telah berubah, dan melewatkan sisanya. Dengan melewatkan semua fungsi atau lambda yang tidak mengalami perubahan parameter, Compose dapat merekomposisi secara efisien.

Jangan pernah bergantung pada efek samping dari menjalankan fungsi yang dapat dikomposisi, karena komposisi ulang suatu fungsi mungkin dilewatkan. Jika Anda melakukannya, pengguna mungkin mengalami perilaku yang aneh dan tidak dapat diprediksi di aplikasi Anda. Efek samping adalah setiap perubahan yang terlihat oleh bagian aplikasi lainnya. Misalnya, semua tindakan ini adalah efek samping yang berbahaya:

  • Menulis ke properti sebuah objek bersama
  • Mengupdate objek yang dapat diamati di ViewModel
  • Mengupdate preferensi bersama

Fungsi yang dapat dikomposisi dapat dijalankan kembali sesering setiap frame, misalnya ketika animasi dirender. Fungsi yang dapat dikomposisi harus cepat untuk menghindari jank selama animasi. Jika Anda perlu melakukan operasi yang mahal, seperti membaca dari preferensi bersama, lakukan di coroutine latar belakang dan teruskan hasil nilai ke fungsi yang dapat dikomposisi sebagai parameter.

Sebagai contoh, kode ini membuat komposisi untuk mengupdate nilai dalam SharedPreferences. Komposisi tidak boleh membaca atau menulis dari preferensi bersama itu sendiri. Sebagai gantinya, kode ini memindahkan operasi baca dan tulis ke ViewModel di coroutine latar belakang. Logika aplikasi meneruskan nilai saat ini dengan callback untuk memicu update.

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

Dokumen ini membahas sejumlah hal yang perlu diketahui saat Anda menggunakan Compose:

  • Rekomposisi melewatkan sebanyak mungkin fungsi dan lambda yang dapat dikomposisi.
  • Rekomposisi bersifat optimistis dan dapat dibatalkan.
  • Fungsi composable dapat dijalankan cukup sering, sesering setiap frame sebuah animasi.
  • Fungsi yang dapat dikomposisi bisa berjalan secara paralel.
  • Fungsi yang dapat dikomposisi bisa berjalan dalam urutan apa pun.

Bagian berikut ini akan membahas cara membuild fungsi yang dapat dikomposisi untuk mendukung rekomposisi. Dalam setiap kasus, praktik terbaiknya adalah menjaga fungsi yang dapat dikomposisi agar tetap cepat, idempoten, dan bebas efek samping.

Rekomposisi melewatkan sebanyak mungkin

Saat sebagian UI Anda tidak valid, Compose akan melakukan yang terbaik untuk merekomposisi bagian yang perlu diupdate saja. Ini berarti Compose dapat melewatkan untuk menjalankan ulang satu komposisi Tombol tanpa menjalankan komposisi mana pun di atas atau di bawahnya dalam hierarki UI.

Setiap fungsi composable dan lambda dapat merekomposisi dengan sendirinya. Berikut ini contoh yang menunjukkan cara rekomposisi dapat melewatkan beberapa elemen saat merender daftar:

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.bodyLarge)
        HorizontalDivider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

Setiap cakupan ini mungkin menjadi satu-satunya hal yang harus dijalankan selama rekomposisi. Compose mungkin akan langsung menuju lambda Column tanpa menjalankan induk mana pun saat header berubah. Dan saat menjalankan Column, Compose mungkin akan memilih untuk melewati item LazyColumn jika names tidak berubah.

Sekali lagi, menjalankan semua fungsi composable atau lambda harus bebas efek samping. Saat Anda perlu menjalankan efek samping, picu efek tersebut dari callback.

Rekomposisi bersifat optimis

Rekomposisi dimulai setiap kali Compose menganggap bahwa parameter sebuah komposisi telah berubah. Rekomposisi bersifat optimis, yang berarti Compose mengharapkan penyelesaian rekomposisi sebelum parameter berubah lagi. Jika parameter memang berubah sebelum rekomposisi selesai, Compose mungkin akan membatalkan rekomposisi dan memulai ulang dengan parameter baru.

Saat rekomposisi dibatalkan, Compose akan menghapus hierarki UI dari rekomposisi tersebut. Jika Anda memiliki efek samping yang bergantung pada UI dan ditampilkan, efek samping itu akan diterapkan meskipun komposisi dibatalkan. Hal ini dapat menyebabkan status aplikasi tidak konsisten.

Pastikan semua fungsi dan lambda yang dapat dikomposisi bersifat idempoten dan bebas efek samping untuk menangani rekomposisi yang optimis.

Fungsi composable bisa berjalan cukup sering

Dalam beberapa kasus, fungsi yang dapat dikomposisi bisa berjalan untuk setiap frame animasi UI. Jika fungsi menjalankan operasi yang mahal, seperti membaca dari penyimpanan perangkat, fungsi tersebut dapat menyebabkan jank UI.

Misalnya, jika widget Anda mencoba membaca setelan perangkat, widget tersebut berpotensi membaca setelan itu ratusan kali per detik, dengan efek yang membahayakan performa aplikasi Anda.

Jika fungsi composable memerlukan data, fungsi tersebut harus menentukan parameter untuk data. Kemudian, Anda dapat memindahkan tugas yang mahal ke thread lainnya, di luar komposisi, dan meneruskan data ke Compose menggunakan mutableStateOf atau LiveData.

Fungsi composable dapat dijalankan secara paralel

Compose dapat mengoptimalkan rekomposisi dengan menjalankan fungsi composable secara paralel. Hal ini akan memungkinkan Compose memanfaatkan beberapa core, dan menjalankan fungsi composable yang tidak ada di layar pada prioritas yang lebih rendah.

Dengan pengoptimalan ini, fungsi composable dapat berjalan dalam kumpulan thread latar belakang. Jika fungsi yang dapat dikomposisi memanggil fungsi di ViewModel, Compose dapat memanggil fungsi tersebut dari beberapa thread sekaligus.

Untuk memastikan aplikasi Anda berperilaku dengan benar, semua fungsi yang dapat dikomposisi tidak boleh memiliki efek samping. Sebaliknya, picu efek samping dari callback seperti onClick yang selalu berjalan pada UI thread.

Saat fungsi yang dapat dikomposisi dipanggil, pemanggilan mungkin terjadi pada thread yang berbeda dari pemanggil. Itu berarti kode yang mengubah variabel dalam lambda yang dapat dikomposisi harus dihindari–karena kode itu tidak aman untuk thread, dan karena merupakan efek samping yang tidak diizinkan dari lambda yang dapat dikomposisi.

Berikut adalah contoh yang menunjukkan komposisi yang menampilkan sebuah daftar dan jumlahnya:

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

Kode ini bebas efek samping, dan mengubah daftar input ke UI. Ini adalah kode yang bagus untuk menampilkan daftar kecil. Namun, jika fungsi menulis ke variabel lokal, kode ini tidak akan aman untuk thread atau benar:

@Composable
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Card {
                    Text("Item: $item")
                    items++ // Avoid! Side-effect of the column recomposing.
                }
            }
        }
        Text("Count: $items")
    }
}

Dalam contoh ini, items dimodifikasi dengan setiap rekomposisi. Itu bisa berupa setiap frame pada animasi, atau saat daftar diupdate. Yang mana pun itu, UI akan menampilkan jumlah yang salah. Oleh karena itu, penulisan seperti ini tidak didukung dalam Compose; dengan melarang penulisan tersebut, kami mengizinkan framework untuk mengubah thread guna menjalankan lambda yang dapat dikomposisi.

Fungsi composable bisa berjalan dalam urutan apa pun

Jika Anda melihat kode untuk fungsi yang dapat dikomposisi, Anda mungkin menganggap bahwa kode berjalan sesuai urutan kode yang muncul. Namun, hal ini tidak dijamin benar. Jika fungsi composable berisi panggilan ke fungsi composable lain, fungsi tersebut dapat berjalan dalam urutan apa pun. Compose memiliki opsi untuk mengenali bahwa beberapa elemen UI memiliki prioritas yang lebih tinggi daripada yang lain, dan akan menggambarnya terlebih dahulu.

Misalnya, Anda memiliki kode seperti ini untuk menggambar tiga layar dalam tata letak tab:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

Panggilan ke StartScreen, MiddleScreen, dan EndScreen dapat terjadi dalam urutan apa pun. Artinya, Anda tidak dapat, misalnya, membuat StartScreen() menetapkan beberapa variabel global (efek samping) dan membuat MiddleScreen() memanfaatkan perubahan tersebut. Sebaliknya, setiap fungsi tersebut harus mandiri.

Pelajari lebih lanjut

Untuk mempelajari lebih lanjut paradigma Compose dan fungsi composable, lihat referensi tambahan berikut.

Video