許多應用程式都需要顯示項目集合。本文說明如何在 Jetpack Compose 中有效率地完成這項操作。
如果您知道用途不需要捲動,不妨使用簡單的 Column
或 Row
(視方向而定),然後逐一對應清單內容,例如:
@Composable
fun MessageList(messages: List<Message>) {
Column {
messages.forEach { message ->
MessageRow(message)
}
}
}
您可以使用 verticalScroll()
修飾元,讓 Column
捲動。詳情請參閱手勢說明文件。
Lazy composables
如果您需要顯示大量項目 (或不明長度的清單),使用 Column
等版面配置可能會導致效能問題,因為無論它們是否可見,所有項目都將被組合和佈局。
Compose 提供一組元件,這個元件只會撰寫和編排要顯示在元件可視區域中的項目。這些元件包含 LazyColumn
和 LazyRow
。
顧名思義,LazyColumn
和 LazyRow
的差異在於其擺放方向,也就是項目和捲動畫面。LazyColumn
會產生垂直捲動清單,LazyRow
則會產生水平捲動清單。
「延遲」元件與 Compose 的大多數版面配置不同。請勿接受@Composable
內容區塊參數可讓應用程式直接發出可組合元件,惰性組件提供了一個 LazyListScope.()
區塊。這個 LazyListScope
區塊提供 DSL,可讓應用程式「說明」項目內容。延遲元件會依照版面配置和捲動位置的需求,新增各個項目的內容。
LazyListScope DSL
LazyListScope
的 DSL 提供多個函式來描述版面配置中的項目。最基本的 item()
會加入單一項目,items(Int)
則會新增多個項目:
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")
}
}
還有一些擴充功能函式可讓您新增項目集合,例如 List
。這些擴充功能讓我們能夠輕鬆遷移上方的 Column
範例:
import androidx.compose.foundation.lazy.items
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(messages) { message ->
MessageRow(message)
}
}
}
此外,items()
擴充功能的變化版本也稱為itemsIndexed()
,可提供索引。詳情請參閱 LazyListScope
參考資料。
內容間距
在某些情況下,您必須在內容邊緣周圍加上邊框間距。延遲元件可讓您將 PaddingValues
傳送至 contentPadding
參數來支援以下功能:
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
// ...
}
在這個範例中,我們加入了水平邊框 (左側和右側) 的 16.dp
和內容頂端及底部的 8.dp
。
請注意,此邊框間距會套用到「內容」,而非 LazyColumn
本身。在上述範例中,第一個項目會在新增 8.dp
邊框間距至其頂部,最後在其底部新增 8.dp
,且所有項目的左側和右側都將具有 16.dp
邊框間距。
Content spacing
如要在項目之間加入間距,您可以使用 Arrangement.spacedBy()
。以下範例會在每個項目之間插入 4.dp
的空間:
LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
// ...
}
同樣地,LazyRow
的情況如下:
LazyRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
// ...
}
項目動畫
如果您使用了 RecyclerView 小工具,就可以知道該功能會自動變更動畫項目。「延遲」版面配置「尚未」提供該功能,這表示項目變更會導致即時「snap」。您可以追蹤這個錯誤,追蹤這項功能的任何變更。
固定式標頭(實驗功能)
顯示分組資料清單時,「固定式標頭」模式相當實用。您可以參考下方的「聯絡人清單」範例,並按照每位聯絡人的初始名稱分組:
如要利用 LazyColumn
實現固定式標頭,您可以使用實驗的 stickyHeader()
函式提供標題內容:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
LazyColumn {
stickyHeader {
Header()
}
items(items) { item ->
ItemRow(item)
}
}
}
若您想取得具有多個標頭的清單 (例如上方的「聯絡人清單」範例),可以:
// 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(實驗功能)
LazyVerticalGrid
元件提供實驗支援,以格狀方式顯示項目。
cells
參數可控制儲存格構成資料欄的方式。以下範例顯示格狀檢視中的項目,並使用 GridCells.Adaptive
將每個資料欄設為至少 128.dp
寬:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 128.dp)
) {
items(photos) { photo ->
PhotoItem(photo)
}
}
}
如果您知道要使用的資料欄數量,可以改為提供包含必要資料欄數量的 GridCells.Fixed
例項。
捲動至捲動位置
許多應用程式都必須回應並聽取捲動位置與項目版面配置變更。延遲元件能藉由傳遞 LazyListState
來支援這種用途:
@Composable
fun MessageList(messages: List<Message>) {
// Remember our own LazyListState
val listState = rememberLazyListState()
// Provide it to LazyColumn
LazyColumn(state = listState) {
// ...
}
}
對於簡易的用途,應用程式通常只需要瞭解第一個可見項目的相關資訊。如要 LazyListState
提供 firstVisibleItemIndex
以及 firstVisibleItemScrollOffset
屬性。
假設 Google 會根據使用者捲動過第一個項目時顯示及隱藏按鈕,而以下列做法為例:
@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()
}
}
}
當您需要更新其他 UI 複合元素時,直接在組合中讀取狀態會很有用,但在某些情況下,特定事件不需要透過相同組合來處理。常見的情形是,當使用者捲動超過某個時間點時,就會傳送分析事件。為了有效處理這種情況,我們可以使用 snapshotFlow()
:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
LazyListState
也會透過 layoutInfo
屬性提供目前正在顯示的項目與這些項目的邊界資訊。詳情請參閱 LazyListLayoutInfo
類別。
控制捲動位置
除了回應捲動位置以外,應用程式也可以控制捲動位置也相當實用。LazyListState
支援scrollToItem()
函式「立即」會貼齊捲動位置,且animateScrollToItem()
使用動畫捲動 (也稱為流暢捲動):
@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)
}
}
)
}
大型資料集(分頁)
分頁程式庫可讓應用程式支援大量項目清單,並視需要載入清單上的小區塊。分頁 3.0 以上版本會透過 androidx.paging:paging-compose
程式庫提供 Compose 支援。
如要顯示分頁內容清單,可以使用collectAsLazyPagingItems()
擴充功能,然後在 LazyColumn
中將傳遞傳回的LazyPagingItems
傳給 items()
。與資料檢視的分頁支援類似,您可以透過檢查 item
是否為 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()
}
}
}
}
項目鍵
根據預設,每個項目的狀態都會與項目在清單中的位置有關。但是,如果資料集變更,就可能會導致問題,因為變更位置的項目會有效保留任何已記住的狀態。如果想像在 LazyColumn
中的 LazyRow
情境,只要資料列變更項目位置,使用者就會失去該資料列的捲動位置。
如要解決這個問題,請為每項項目提供穩定的專屬金鑰,為 key
參數提供區塊。提供固定的索引鍵可讓項目狀態在資料集變更保持一致:
@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)
}
}
}