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 menyetel 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.
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 yang dapat dikomposisi dari
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 yang dapat dikomposisi harus ditulis dengan properti ini, untuk alasan yang dibahas dalam Rekomposisi.
- 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
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.
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.
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:
- Fungsi yang dapat dikomposisi bisa berjalan dalam urutan apa pun.
- Fungsi yang dapat dikomposisi bisa berjalan secara paralel.
- 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.
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.
Fungsi yang dapat dikomposisi 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, ini belum tentu 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.
Fungsi composable dapat berjalan secara paralel
Compose dapat mengoptimalkan rekomposisi dengan menjalankan fungsi composable secara paralel. Hal ini memungkinkan Compose memanfaatkan multi-core, dan menjalankan fungsi yang dapat dikomposisi yang tidak ada di layar pada prioritas yang lebih rendah.
Dengan pengoptimalan ini, fungsi yang dapat dikomposisi bisa 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 @Deprecated("Example with bug") fun ListWithBug(myList: List<String>) { var items = 0 Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { 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.
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) Divider() // 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. 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 yang dapat dikomposisi memerlukan data, fungsi itu harus menentukan parameter untuk data tersebut.
Kemudian, Anda dapat memindahkan tugas yang mahal ke thread lainnya, di luar
komposisi, dan meneruskan data ke Compose menggunakan mutableStateOf
atau LiveData
.
Mempelajari lebih lanjut
Untuk mempelajari lebih lanjut paradigma Compose dan fungsi composable, lihat referensi tambahan berikut.
Video
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Kotlin untuk Jetpack Compose
- Status dan Jetpack Compose
- Lapisan arsitektur Jetpack Compose