Daftar

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 seperti:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

Kita dapat membuat Column dapat di-scroll dengan menggunakan pengubah verticalScroll(). Lihat dokumentasi Gestur untuk mengetahui informasi selengkapnya.

Composable lambat

Jika Anda perlu menampilkan sejumlah besar item (atau daftar panjang yang tidak diketahui), menggunakan tata letak seperti Column dapat menyebabkan masalah performa, karena semua produk akan dikomposisi 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 memancarkan komponen, 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

@Composable
fun MessageList(messages: List<Message>) {
    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.

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),
) {
    // ...
}

Animasi item

Jika menggunakan widget RecyclerView, Anda akan mengetahui bahwa widget tersebut menganimasikan perubahan item secara otomatis. Tata letak Lambat belum menyediakan fungsi tersebut, yang berarti perubahan item menyebabkan 'snap' instan. Anda dapat mengikuti bug ini untuk melacak perubahan pada fitur ini.

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:

Video ponsel yang di-scroll ke atas dan ke bawah melalui daftar 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 melakukan:

// TODO: 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)
            }
        }
    }
}

Grid (eksperimental)

Composable LazyVerticalGrid dapat memberikan dukungan eksperimental untuk menampilkan item dalam grid.

Screenshot ponsel yang menampilkan grid foto

Parameter cells mengontrol cara sel terbentuk menjadi kolom. Contoh berikut menampilkan item dalam grid, menggunakan GridCells.Adaptive untuk menetapkan lebar masing-masing kolom minimal 128.dp:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        cells = GridCells.Adaptive(minSize = 128.dp)
    ) {
        items(photos) { photo ->
            PhotoItem(photo)
        }
    }
}

Jika mengetahui jumlah kolom yang akan digunakan, Anda dapat memberikan instance GridCells.Fixed yang berisi jumlah kolom yang diperlukan.

Bereaksi terhadap posisi scroll

Banyak aplikasi yang perlu bereaksi dan mendengarkan untuk men-scroll perubahan tata letak item dan posisi. Komponen lambat mendukung kasus penggunaan ini dengan mengangkat 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 tombol menampilkan dan menyembunyikan berdasarkan apakah pengguna telah men-scroll melewati item pertama:

@OptIn(ExperimentalAnimationApi::class) // AnimatedVisibility
@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 == true }
        .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:

import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyPagingItems) { message ->
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

Kunci item

Secara default, setiap status item dimasukkan ke posisi item dalam daftar. 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:

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(
            items = messages,
            key = { message ->
                // Return a stable + unique key for the item
                message.id
            }
        ) { message ->
            MessageRow(message)
        }
    }
}