許多應用程式都需要顯示項目集合。本文將說明如何在 Jetpack Compose 中有效率地進行這項操作。
如果您知道在自己的使用情境中無需捲動功能,不妨使用簡單的 Column
或 Row
(視方向而定),然後透過疊代清單來發出各個項目,如下所示:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
您可以使用 verticalScroll()
修飾符讓 Column
可捲動。
Lazy 清單
如果您需要顯示大量項目 (或長度未知的清單),
使用 Column
這類版面配置可能會導致效能問題,因為無論這些項目是否顯示,系統都會加以組合和配置。
Compose 提供一組元件,這些元件只會撰寫和配置項目
都能顯示在元件可視區域中這些元件包括
LazyColumn
和
LazyRow
。
顧名思義,
LazyColumn
和
LazyRow
是項目版面配置及捲動的方向。LazyColumn
會產生垂直捲動清單,LazyRow
則會產生水平捲動清單。
Lazy 元件與 Compose 的大多數版面配置不同。而不是
接受 @Composable
內容區塊參數,讓應用程式直接
發出可組合函式,Lazy 元件則提供 LazyListScope.()
區塊。這個 LazyListScope
區塊提供 DSL,可讓應用程式說明項目內容。
接著,Lazy 元件會負責新增每個項目的內容
版面配置和捲動位置所需的標記
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 */ LazyColumn { items(messages) { message -> MessageRow(message) } }
此外,此外,
items()
呼叫了擴充功能函式
itemsIndexed()
、
並提供索引詳情請參閱 LazyListScope
參考資料。
Lazy 格線
LazyVerticalGrid
和
LazyHorizontalGrid
可組合函式支援在格線中顯示項目。Lazy 垂直格線會在橫跨多欄的垂直捲動式容器中顯示項目,而 Lazy 水平格線在水平軸上會有相同的行為。
格線與清單具有相同的 API 功能,而且還使用
相當相似的 DSL
LazyGridScope.()
描述內容
LazyVerticalGrid
中的 columns
參數和 LazyHorizontalGrid
中的 rows
參數可控管儲存格轉換為欄或列的方式。下列
範例會使用
GridCells.Adaptive
將每欄的寬度設為至少 128.dp
:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
可讓您指定項目寬度,然後格線會
盡可能容納更多欄計算欄數後,所有剩餘的寬度會平均分配給各欄。這種自動調整尺寸的方式特別適合用來顯示項目組合
支援各種螢幕大小
如果您知道要使用的確切欄數,可以改為提供
執行個體
GridCells.Fixed
。
如果設計只有某些項目需要非標準尺寸
您可以使用格狀支援功能,為項目提供自訂欄時距。
使用 LazyGridScope DSL
item
和 items
方法的 span
參數指定欄時距。maxLineSpan
,
其中一個時距範圍的值,當您在使用
自動調整大小,因為欄數沒有固定。
以下範例說明如何提供完整的資料列時距:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
延遲交錯方格
LazyVerticalStaggeredGrid
和
LazyHorizontalStaggeredGrid
這些可組合函式可用來建立延遲載入、交錯顯示的項目格線。
Lazy 垂直交錯格線會在可垂直捲動的環境中顯示項目
這個容器橫跨多個資料欄
不同的高度Lazy 水平格線在水平軸上會顯示不同寬度的項目,並且具有相同的行為。
以下程式碼片段是使用 LazyVerticalStaggeredGrid
的基本範例
每個項目的寬度為 200.dp
:
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() )
如要設定固定的欄數,您可以使用 StaggeredGridCells.Fixed(columns)
而非 StaggeredGridCells.Adaptive
。這會將可用的寬度除以欄數 (或水平格線的列數),並讓每個項目占用該寬度 (或水平格線的高度):
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() )
內容間距
您有時候必須在內容邊緣周圍加上邊框間距,延遲元件可讓您將一些 PaddingValues
傳遞至 contentPadding
參數來支援這項功能:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
在這個範例中,我們在水平邊緣 (左側和右側) 加入 16.dp
邊框間距,然後在內容的頂端和底部加入 8.dp
。
請注意,這個邊框間距會套用到內容,而非
LazyColumn
。在上述範例中,第一個項目會新增 8.dp
邊框間距,最後一個項目會在底部加入 8.dp
,以及所有項目
左側和右側會有 16.dp
的邊框間距。
內容間距
如要在項目之間加入間距,可以使用
Arrangement.spacedBy()
。
以下範例會在每個項目之間加入 4.dp
的間距:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
LazyRow
的使用方式類似:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
不過,格線接受垂直和水平排列方式:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
項目鍵
根據預設,每個項目的狀態都會與項目在
或格狀檢視不過,如果資料集有變動,則可能會造成問題,這是因為項目
變更位置實際上會遺失任何已記住的狀態假設
LazyColumn
中的 LazyRow
情境 (如果資料列變更項目位置),
使用者的捲動位置就會消失在列中。
如要解決這個問題,您可以為每個項目提供穩定專屬鍵,提供
key
參數區塊。提供穩定的鍵可讓項目狀態在資料集發生變更時保持一致:
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
只要提供鍵,就能讓 Compose 正確重新排序。舉例來說,如果項目含有已記住的狀態,設定鍵即可 當項目的位置改變時,Compose 即可隨著項目的位置一起移動此狀態。
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
不過,可當做項目鍵使用的類型有一項限制。
金鑰的類型必須是由
Bundle
,這是 Android 的
狀態。Bundle
支援基元等類型
列舉或 Parcelable
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
金鑰必須由 Bundle
支援,以便內部的 rememberSaveable
重新建立 Activity 時即可還原商品可組合函式
您向外捲動畫面並捲動回頭,
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
項目動畫
如果用過 RecyclerView 小工具,就會知道該小工具會為項目建立動畫效果
變更。
Lazy 版面配置提供相同的項目重新排序功能。該 API 原理簡單 - 您只需要將 animateItemPlacement
修飾元設為項目內容即可:
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()) { // ... } } }
如有下列需求,您甚至可以提供自訂動畫規格:
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) ) ) { // ... } } }
確定您為項目提供鍵,以便系統為移動的元素找到新的位置。
固定式標頭 (實驗功能)
顯示分組資料清單時,「固定式標頭」模式相當實用。下方為「聯絡人清單」範例,其中以每位聯絡人的名稱首字母分組:
如要透過 LazyColumn
建立固定式標頭,您可以使用實驗功能
stickyHeader()
函式,提供標頭內容:
@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
如果要開啟含有多個標題的清單 (例如上述的「聯絡人清單」範例), 建議做法:
// 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) } } } }
回應捲動位置
許多應用程式都必須回應並監聽捲動位置與項目版面配置變更。Lazy 元件可以藉由提升
LazyListState
:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
針對簡易的使用情境,應用程式通常只需要瞭解
第一個顯示的項目為此,LazyListState
提供 firstVisibleItemIndex
和 firstVisibleItemScrollOffset
屬性。
以下範例會根據使用者是否曾捲動經過第一個項目顯示及隱藏按鈕:
@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 可組合項時,直接在組合中讀取狀態會很有用,但在某些情況下,事件不需要在相同組合中來處理。常見的例子是
當使用者捲動網頁經過某個時間點時,就會產生 Analytics 事件。為了處理這種情況
因此可以使用
snapshotFlow()
:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .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()
然後再傳入傳回的
LazyPagingItems
修改了《LazyColumn
》中的 items()
。與檢視畫面的分頁支援類似,您可以
檢查 item
是否為 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() } } } }
使用 Lazy 版面配置的提示
以下提供幾個訣竅,協助您確保 Lazy 版面配置可以正常運作。
避免使用 0 像素尺寸的項目
這種情況可能會發生在例如預期以非同步方式 擷取圖片等一些資料,以便日後填入清單項目。 這樣會導致 Lazy 版面配置在第一個情況下組合所有項目 高度,因為高度為 0 像素,可以容納所有 檢視區域當項目載入並展開其高度後,Lazy 版面配置就會捨棄所有其他在首次中不必要地組合的項目,因為實際上無法調整至可視區域內。為避免這種情況發生 您應該設定項目的預設尺寸,讓 Lazy 版面配置可以 計算可視區域中實際可容納的項目數量:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
在系統以非同步方式載入資料後,當您知道項目的約略尺寸時,良好的做法是,確保項目的尺寸在載入前後保持不變 (例如透過加入一些預留位置做到這點)。這有助於維持正確的捲動位置。
避免以巢狀方式嵌入可往相同方向捲動的元件
這種做法僅適用於以下情況:將可捲動子項以巢狀方式嵌入另一個可往相同方向捲動的父項內,且並未預先定義尺寸。例如,嘗試以巢狀方式載入在垂直可捲動的 Column
父項內沒有固定高度的子項 LazyColumn
:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
相反地,納入所有可組合函式也能達到相同結果
在一個父項 LazyColumn
中,然後使用其 DSL 傳入不同類型的
內容。這樣可以在同一個位置發出單一項目和多個清單項目:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
請注意,您可以以巢狀方式載入不同方向的版面配置,例如可捲動的父項 Row
和子項 LazyColumn
:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
您也可繼續使用相同方向的版面配置,同時為巢狀子項設定固定尺寸:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
將多個元素放在同一項目中時的注意事項
在這個範例中,第二個項目 lambda 會在一個區塊中發出 2 個項目:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
Lazy 版面配置會按正常方式處理此情況,也就是依序排進這些元素,就像這些元素是不同項目一樣。不過 可能會產生一些問題
將多個元素當做某個項目的一部分發出時,系統會將這些元素視為
代表不能再個別組成如果其中一個元素可以顯示在畫面上,則所有與該項目相對應的元素都必須經過組合和測量。過度使用時,這可能會對效能造成負面影響。萬一所有元素都放在同一個項目中,就完全無法達到使用 Lazy 版面配置的目的。除了潛在的效能問題之外,將多個元素放入同一個項目中也會干擾 scrollToItem()
和 animateScrollToItem()
。
不過,將多個元素放在同一個項目中也行得通 就像在清單中加入分隔線一樣您不希望分隔線變更捲動方式 因為這類索引不應視為獨立元素。另外,效能 分隔線較小。分隔線可能需要 因為項目尚未顯示,就屬於前一個項目 項目:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
考慮使用自訂排列方式
一般來說,Lazy 清單內含許多項目,其占用空間會超出 。不過,如果名單中填入的項目不多, 設計可能會有更具體的定位需求 檢視點
為此,您可以使用自訂垂直 Arrangement
,並傳送至 LazyColumn
。在以下範例中,TopWithFooter
物件只需要實作 arrange
方法即可。首先
依序排列其次,如果已使用的總高度低於可視區域高度,該方法會將頁尾置於底部:
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() } } }
建議加入 contentType
從 Compose 1.2 開始,盡可能提高 Lazy 的效能
不妨考慮加入
contentType
加入清單或格線因此您可以指定各個
版面配置的項目;當您撰寫清單或格線時,
幾種不同的項目類型
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
當您提供
contentType
,
Compose 只能重複使用組合
相同類型的項目由於提供內容類型可確保 Compose 不會嘗試在 A 類型項目的上方組合完全不同的 B 類型項目,因此在組合結構類似的項目時,可透過重複使用的做法提升效率。這麼做可以充分發揮組合的優點
重複使用 以及 Lazy 版面配置的效能
評估成效
只有在執行 版本模式,並啟用 R8 最佳化功能在偵錯版本中,Lazy 版面配置捲動顯示的速度可能較慢。如需更多相關資訊,請詳閱 Compose 效能。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 將
RecyclerView
遷移至 Lazy 清單 - 在 Compose 中儲存 UI 狀態
- 適用於 Jetpack Compose 的 Kotlin