Banyak aplikasi perlu menampilkan koleksi item. Dokumen ini menjelaskan bagaimana Anda dapat melakukan ini secara efisien di Jetpack Compose.
Jika Anda tahu bahwa kasus penggunaan Anda tidak memerlukan scroll, Anda dapat menggunakan Column
atau Row
sederhana (bergantung pada arah), dan memunculkan setiap konten item dengan
mengiterasi daftar dengan cara berikut:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
Kita dapat membuat Column
dapat di-scroll dengan menggunakan pengubah verticalScroll()
.
Daftar lambat
Jika Anda perlu menampilkan item dalam jumlah yang banyak (atau daftar dengan panjang yang tidak diketahui),
menggunakan tata letak seperti Column
dapat menyebabkan masalah performa, karena semua item akan disusun dan ditata baik terlihat atau tidak.
Compose menyediakan kumpulan komponen yang hanya mengomposisi dan menata letak item yang
terlihat di area pandang komponen. Komponen ini meliputi LazyColumn
dan LazyRow
.
Seperti namanya, perbedaan antara LazyColumn
dan LazyRow
adalah pada orientasi tata letak item dan scroll. LazyColumn
menghasilkan daftar scroll vertikal, dan LazyRow
menghasilkan daftar
scroll horizontal.
Komponen Lambat berbeda dengan sebagian besar tata letak di Compose. Alih-alih
menerima parameter blok konten @Composable
, yang memungkinkan aplikasi langsung
memunculkan composable, komponen Lambat menyediakan blok LazyListScope.()
. Blok
LazyListScope
ini menawarkan DSL yang memungkinkan aplikasi menjelaskan konten item. Komponen Lambat kemudian bertanggung jawab untuk menambahkan konten setiap item seperti yang diwajibkan oleh tata letak dan posisi scroll.
DSL LazyListScope
DSL LazyListScope
menyediakan sejumlah fungsi untuk mendeskripsikan item dalam tata letak. Pada kasus yang paling dasar, item()
menambahkan satu item, dan items(Int)
menambahkan beberapa item:
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
Ada juga sejumlah fungsi ekstensi yang memungkinkan Anda menambahkan koleksi item, seperti List
. Ekstensi ini memungkinkan kita memigrasikan
contoh Column
dengan mudah dari atas:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
Ada juga varian dari fungsi ekstensi
items()
yang disebut
itemsIndexed()
,
yang menyediakan indeks. Lihat referensi
LazyListScope
untuk detail selengkapnya.
Petak lambat
Composable
LazyVerticalGrid
dan
LazyHorizontalGrid
memberikan dukungan untuk menampilkan item dalam petak. Petak vertikal Lambat
akan menampilkan itemnya dalam penampung yang dapat di-scroll secara vertikal, yang dibentangkan
di beberapa kolom, sedangkan petak horizontal Lambat akan memiliki perilaku yang sama
pada sumbu horizontal.
Petak memiliki kemampuan API canggih yang sama dengan daftar dan juga menggunakan
DSL yang sangat mirip -
LazyGridScope.()
untuk mendeskripsikan konten.
Parameter columns
di
parameter LazyVerticalGrid
dan rows
di
LazyHorizontalGrid
mengontrol cara sel terbentuk menjadi kolom atau baris. Contoh berikut
menampilkan item dalam petak, menggunakan
GridCells.Adaptive
untuk menetapkan lebar masing-masing kolom minimal 128.dp
:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
memungkinkan Anda menentukan lebar item, lalu petak akan
sesuai dengan sebanyak mungkin kolom. Lebar apa pun yang tersisa didistribusikan secara merata
di antara kolom, setelah jumlah kolom dihitung.
Cara adaptif pengukuran ini sangat berguna untuk menampilkan kumpulan item
di berbagai ukuran layar.
Jika mengetahui jumlah kolom yang akan digunakan, Anda dapat memberikan
instance
GridCells.Fixed
yang berisi jumlah kolom yang diperlukan.
Jika desain Anda hanya memerlukan item tertentu untuk memiliki dimensi non-standar,
Anda dapat menggunakan dukungan petak untuk menyediakan span kolom kustom untuk item.
Tentukan span kolom dengan parameter span
dari metode
item
dan items
LazyGridScope DSL
.
maxLineSpan
,
salah satu nilai cakupan span, sangat berguna saat Anda menggunakan
ukuran adaptif, karena jumlah kolom tidak tetap.
Contoh ini menunjukkan cara memberikan span baris lengkap:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
Petak bertahap lambat
LazyVerticalStaggeredGrid
dan
LazyHorizontalStaggeredGrid
adalah composable yang memungkinkan Anda membuat petak item yang dimuat lambat dan bertingkat.
Petak bertahap vertikal lambat menampilkan itemnya dalam penampung yang dapat di-scroll secara vertikal
yang membentang di beberapa kolom dan memungkinkan setiap item memiliki
tinggi yang berbeda. Petak horizontal lambat memiliki perilaku yang sama pada
sumbu horizontal dengan item yang memiliki lebar berbeda.
Cuplikan berikut adalah contoh dasar penggunaan LazyVerticalStaggeredGrid
dengan lebar 200.dp
per item:
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(200.dp), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Untuk menetapkan jumlah kolom tetap, Anda dapat menggunakan
StaggeredGridCells.Fixed(columns)
, bukan StaggeredGridCells.Adaptive
.
Fungsi ini membagi lebar yang tersedia dengan jumlah kolom (atau baris untuk
petak horizontal), dan meminta setiap item mengambil lebar tersebut (atau tinggi untuk
petak horizontal):
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(3), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
Padding konten
Terkadang Anda perlu menambahkan padding di sekitar tepi konten. Komponen
lambat memungkinkan Anda meneruskan
PaddingValues
tertentu ke parameter contentPadding
untuk mendukung ini:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
Dalam contoh ini, kami menambahkan 16.dp
padding ke tepi horizontal (kiri dan
kanan), lalu 8.dp
ke bagian atas dan bawah konten.
Perhatikan bahwa padding ini berlaku untuk konten, bukan untuk LazyColumn
itu sendiri. Pada contoh di atas, item pertama akan menambahkan padding 8.dp
ke bagian atas, item terakhir akan menambahkan 8.dp
ke bagian bawah, dan semua item akan memiliki padding 16.dp
di kiri dan kanan.
Jarak konten
Untuk menambahkan spasi di antara item, Anda dapat menggunakan Arrangement.spacedBy()
.
Contoh di bawah ini menambahkan 4.dp
spasi di antara setiap item:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Demikian pula untuk LazyRow
:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
Namun, petak menerima pengaturan vertikal dan horizontal:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
Kunci item
Secara default, setiap status item dikunci sesuai posisi item dalam
daftar atau petak. Namun, hal ini dapat menyebabkan masalah jika set data berubah, karena item yang
mengubah posisi secara efektif kehilangan status yang diingat. Jika Anda membayangkan skenario LazyRow
dalam LazyColumn
, jika baris mengubah posisi item, pengguna akan kehilangan posisi scroll dalam baris tersebut.
Untuk mengatasi hal ini, Anda dapat memberikan kunci yang stabil dan unik untuk setiap item, dengan memberikan pemblokiran ke parameter key
. Dengan menyediakan kunci yang stabil, status item akan
konsisten di seluruh perubahan set data:
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
Dengan menyediakan kunci, Anda membantu Compose menangani pengurutan ulang dengan benar. Misalnya, jika item Anda berisi status yang diingat, kunci setelan akan memungkinkan Compose memindahkan status ini bersama item tersebut, saat posisinya berubah.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
Namun, ada satu batasan pada jenis yang dapat Anda gunakan sebagai kunci item.
Jenis kunci harus didukung oleh
Bundle
, mekanisme Android untuk mempertahankan
status saat Aktivitas dibuat ulang. Bundle
mendukung jenis seperti primitive,
enum, atau Parcelable.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
Kunci harus didukung oleh Bundle
sehingga rememberSaveable
di dalam
composable item dapat dipulihkan saat Aktivitas dibuat ulang, atau bahkan
saat Anda men-scroll keluar dari item ini dan men-scroll kembali.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
Animasi item
Jika menggunakan widget RecyclerView, Anda akan mengetahui bahwa widget tersebut menganimasikan perubahan
item secara otomatis.
Tata letak lambat menyediakan fungsi yang sama untuk penyusunan ulang item.
API ini sederhana - Anda hanya perlu menetapkan pengubah
animateItem
ke konten item:
LazyColumn { // It is important to provide a key to each item to ensure animateItem() works as expected. items(books, key = { it.id }) { Row(Modifier.animateItem()) { // ... } } }
Anda bahkan dapat memberikan spesifikasi animasi kustom, jika perlu:
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItem( fadeInSpec = tween(durationMillis = 250), fadeOutSpec = tween(durationMillis = 100), placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) ) ) { // ... } } }
Pastikan Anda menyediakan kunci untuk item sehingga Anda dapat menemukan posisi baru untuk elemen yang dipindahkan.
Header sticky (eksperimental)
Pola 'header sticky' berguna saat menampilkan daftar data yang dikelompokkan. Di bawah ini, Anda dapat melihat contoh 'daftar kontak', yang dikelompokkan berdasarkan setiap inisial kontak:
Untuk mendapatkan header sticky dengan LazyColumn
, Anda dapat menggunakan fungsi stickyHeader()
eksperimental, yang menyediakan konten header:
@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
Untuk mencapai daftar dengan beberapa header, seperti contoh 'daftar kontak' di atas, Anda dapat menggunakan:
// This ideally would be done in the ViewModel val grouped = contacts.groupBy { it.firstName[0] } @OptIn(ExperimentalFoundationApi::class) @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
Bereaksi terhadap posisi scroll
Banyak aplikasi yang perlu bereaksi dan memproses perubahan tata letak item dan posisi scroll.
Komponen Lambat mendukung kasus penggunaan ini dengan menarik
LazyListState
:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
Untuk kasus penggunaan sederhana, aplikasi biasanya hanya perlu mengetahui informasi tentang
item pertama yang terlihat. Untuk ini, LazyListState
memberikan properti firstVisibleItemIndex
dan firstVisibleItemScrollOffset
.
Jika kita menggunakan contoh yang menampilkan dan menyembunyikan tombol berdasarkan apakah pengguna telah men-scroll melewati item pertama:
@Composable fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
Membaca status secara langsung dalam komposisi berguna saat Anda perlu mengupdate composable UI lain, tetapi ada juga skenario saat peristiwa tidak perlu ditangani dalam komposisi yang sama. Contoh umum dari hal ini adalah mengirimkan peristiwa analisis setelah pengguna men-scroll melewati titik tertentu. Untuk menangani masalah ini secara efisien, kita dapat menggunakan snapshotFlow()
:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
LazyListState
juga memberikan informasi tentang semua item yang saat ini ditampilkan dan batasnya di layar, melalui properti layoutInfo
. Lihat class LazyListLayoutInfo
untuk mengetahui informasi selengkapnya.
Mengontrol posisi scroll
Selain merespons posisi scroll, sebaiknya aplikasi juga dapat mengontrol posisi scroll.
LazyListState
mendukung ini melalui fungsi scrollToItem()
, yang 'segera' men-snap posisi scroll, dan animateScrollToItem()
yang men-scroll menggunakan animasi (juga dikenal sebagai scroll halus):
@Composable fun MessageList(messages: List<Message>) { val listState = rememberLazyListState() // Remember a CoroutineScope to be able to launch val coroutineScope = rememberCoroutineScope() LazyColumn(state = listState) { // ... } ScrollToTopButton( onClick = { coroutineScope.launch { // Animate scroll to the first item listState.animateScrollToItem(index = 0) } } ) }
Set data besar (paging)
Dengan library Paging, aplikasi dapat mendukung sejumlah besar item, memuat, dan menampilkan potongan daftar kecil sesuai kebutuhan. Paging 3.0 dan yang lebih baru menyediakan dukungan Compose melalui library androidx.paging:paging-compose
.
Untuk menampilkan daftar konten halaman, kita dapat menggunakan fungsi ekstensi collectAsLazyPagingItems()
, lalu meneruskan LazyPagingItems
yang ditampilkan ke items()
di LazyColumn
. Serupa dengan dukungan Paging dalam tampilan, Anda dapat menampilkan placeholder saat data dimuat dengan memeriksa apakah item
adalah null
:
@Composable fun MessageList(pager: Pager<Int, Message>) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it.id } ) { index -> val message = lazyPagingItems[index] if (message != null) { MessageRow(message) } else { MessagePlaceholder() } } } }
Tips menggunakan tata letak Lambat
Ada beberapa tips yang dapat Anda pertimbangkan untuk memastikan tata letak Lambat berfungsi sebagaimana mestinya.
Menghindari penggunaan item berukuran 0 piksel
Hal ini dapat terjadi dalam skenario saat, misalnya, Anda ingin mengambil beberapa data secara asinkron seperti gambar, untuk mengisi item daftar Anda pada tahap berikutnya. Tindakan tersebut akan menyebabkan tata letak Lambat menyusun semua itemnya dalam pengukuran pertama, karena tingginya 0 piksel dan dapat memuat semuanya di area pandang. Setelah item dimuat dan tingginya diperluas, tata letak Lambat akan menghapus semua item lain yang belum perlu disusun untuk pertama kalinya karena sebenarnya tidak dapat menyesuaikan dengan area pandang. Untuk menghindari hal ini, Anda harus menetapkan ukuran default ke item sehingga tata letak Lambat dapat menghitung jumlah item yang benar-benar dapat dimuat di area pandang dengan benar:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
Bila Anda mengetahui perkiraan ukuran item setelah data dimuat secara asinkron, praktik yang baik adalah memastikan ukuran item tetap sama sebelum dan setelah pemuatan, misalnya, dengan menambahkan beberapa placeholder. Ini akan membantu mempertahankan posisi scroll yang benar.
Menghindari penyusunan bertingkat komponen yang dapat di-scroll di arah yang sama
Ini hanya berlaku untuk kasus saat menyusun bertingkat turunan yang dapat di-scroll tanpa ukuran
yang telah ditetapkan di dalam induk yang dapat di-scroll dan memiliki arah sama. Misalnya, mencoba
menyusun bertingkat LazyColumn
turunan tanpa tinggi tetap di dalam induk Column
yang dapat di-scroll secara vertikal:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
Sebaliknya, hasil yang sama dapat dicapai dengan menggabungkan semua composable Anda
di dalam satu LazyColumn
induk dan menggunakan DSL-nya untuk meneruskan berbagai jenis
konten. Hal ini memungkinkan kemunculan item tunggal, serta beberapa item daftar,
semuanya di satu tempat:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
Perlu diingat bahwa saat Anda menyusun tata letak arah yang berbeda secara bertingkat,
misalnya Row
induk yang dapat di-scroll dan LazyColumn
turunan, akan diizinkan:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
Begitu juga dengan saat Anda masih menggunakan tata letak arah yang sama, tetapi juga menetapkan ukuran tetap ke turunan bertingkat:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
Berhati-hati dalam menempatkan beberapa elemen dalam satu item
Dalam contoh ini, lambda item kedua memunculkan 2 item dalam satu blok:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
Tata letak lambat akan menangani hal ini seperti yang diharapkan - tata letak akan menata letak elemen satu per satu seolah-olah merupakan item yang berbeda. Namun, ada beberapa masalah saat melakukannya.
Jika beberapa elemen ditampilkan sebagai bagian dari satu item, elemen tersebut ditangani sebagai
satu entitas, yang berarti elemen tersebut tidak dapat lagi disusun satu per satu. Jika satu
elemen terlihat di layar, semua elemen yang sesuai dengan
item tersebut harus dikomposisi dan diukur. Hal ini dapat mengganggu performa jika digunakan
secara berlebihan. Untuk menempatkan semua elemen dalam satu item secara ekstrem, tujuan
menggunakan tata letak Lambat akan sepenuhnya gagal. Selain potensi
masalah performa, menempatkan lebih banyak elemen dalam satu item juga akan mengganggu
scrollToItem()
& animateScrollToItem()
.
Namun, ada kasus penggunaan yang valid untuk menempatkan beberapa elemen dalam satu item, seperti memiliki pembagi di dalam daftar. Anda tidak ingin pembagi mengubah indeks scroll, karena tidak boleh dianggap sebagai elemen independen. Selain itu, performa tidak akan terpengaruh karena pembagi kecil. Pembagi mungkin perlu terlihat saat item di depannya terlihat, sehingga dapat menjadi bagian dari item sebelumnya:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Mencoba menggunakan pengaturan kustom
Biasanya daftar Lambat memiliki banyak item, dan menempati lebih dari ukuran penampung scroll. Namun, jika daftar diisi dengan sedikit item, desain Anda dapat memiliki persyaratan yang lebih spesifik terkait cara penempatan ini dalam area pandang.
Untuk mencapainya, Anda dapat menggunakan vertical kustom
Arrangement
dan meneruskannya ke LazyColumn
. Pada contoh berikut, objek TopWithFooter
hanya perlu mengimplementasikan metode arrange
. Pertama, item akan diposisikan
satu per satu. Kedua, jika total tinggi yang digunakan lebih rendah dari
tinggi area pandang, footer akan diposisikan di bawah:
object TopWithFooter : Arrangement.Vertical { override fun Density.arrange( totalSize: Int, sizes: IntArray, outPositions: IntArray ) { var y = 0 sizes.forEachIndexed { index, size -> outPositions[index] = y y += size } if (y < totalSize) { val lastIndex = outPositions.lastIndex outPositions[lastIndex] = totalSize - sizes.last() } } }
Mencoba menambahkan contentType
Mulai dari Compose 1.2, untuk memaksimalkan performa tata letak
Lambat, sebaiknya coba tambahkan
contentType
ke daftar atau petak Anda. Hal ini memungkinkan Anda menentukan jenis konten untuk setiap
item tata letak, ketika Anda mengomposisi daftar atau petak yang terdiri
dari beberapa jenis item:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
Saat Anda menyediakan
contentType
,
Compose dapat menggunakan kembali komposisi hanya
di antara item dari jenis yang sama. Karena penggunaan ulang lebih efisien saat Anda
mengomposisi item dengan struktur yang serupa, menyediakan jenis konten akan memastikan
Compose lebih memprioritaskan untuk mengomposisi item berjenis A daripada item yang benar-benar
berbeda dari jenis B. Hal ini membantu memaksimalkan manfaat penggunaan ulang
komposisi dan performa tata letak Lambat.
Mengukur performa
Anda hanya dapat mengukur performa tata letak Lambat dengan andal saat berjalan dalam mode rilis dan dengan pengoptimalan R8 diaktifkan. Pada build debug, scroll tata letak Lambat mungkin tampak lebih lambat. Untuk informasi selengkapnya tentang hal ini, baca Performa Compose.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memigrasikan
RecyclerView
ke daftar Lambat - Menyimpan status UI di Compose
- Kotlin untuk Jetpack Compose