許多應用程式都需要顯示項目集合。本文件將說明 這在 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.()
敬上
描述內容
以下程式碼中的 columns
參數:
LazyVerticalGrid
和 rows
參數
LazyHorizontalGrid
控制儲存格轉換為欄或列的方式。下列
範例會使用
GridCells.Adaptive
敬上
將每欄的寬度設為至少 128.dp
:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid
可讓您指定項目寬度,然後格線會
盡可能容納更多欄剩餘的寬度會均等分配
資料欄數。
這種自動調整尺寸的方式特別適合用來顯示項目組合
支援各種螢幕大小
如果您知道要使用的確切欄數,可以改為提供
執行個體
GridCells.Fixed
敬上
。
如果設計只有某些項目需要非標準尺寸
您可以使用格狀支援功能,為項目提供自訂欄時距。
請使用以下項目的 span
參數指定資料欄時距:
LazyGridScope DSL
item
和 items
方法。
maxLineSpan
、
其中一個時距範圍的值,當您在使用
自動調整大小,因為欄數沒有固定。
以下範例說明如何提供完整的資料列時距:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
延遲交錯方格
LazyVerticalStaggeredGrid
敬上
和
LazyHorizontalStaggeredGrid
。
這些可組合函式可用來建立延遲載入、交錯顯示的項目格線。
Lazy 垂直交錯格線會在可垂直捲動的環境中顯示項目
這個容器橫跨多個資料欄
不同的高度Lazy 水平格線在 Google 地球上
各個寬度的項目
以下程式碼片段是使用 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() )
![在 Compose 中延遲交錯圖片格線](https://developer.android.com/static/develop/ui/compose/images/lists/staggered_grid_fixed.png?authuser=9&hl=zh-tw)
內容間距
有時您必須在內容邊緣周圍加上邊框間距。懶人
可讓您傳送
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 { items(books, key = { it.id }) { Row(Modifier.animateItemPlacement()) { // ... } } }
如有下列需求,您甚至可以提供自訂動畫規格:
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItemPlacement( tween(durationMillis = 250) ) ) { // ... } } }
請務必為商品設定鍵,我們才能為新的項目提供新的鍵 。
除了重新排序之外,目前提供新增和移除項目的項目動畫 仍在開發階段如要追蹤進度,請前往 問題 150812265。
固定式標頭 (實驗功能)
顯示分組資料清單時,「固定式標頭」模式相當實用。 下方是依聯絡人分組的「聯絡人清單」範例 初始:
如要透過 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
資源。
以下範例會根據使用者是否曾捲動經過第一個項目顯示及隱藏按鈕:
@OptIn(ExperimentalAnimationApi::class) @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) } } ) }
大型資料集 (分頁)
分頁程式庫可讓應用程式
支援大型項目清單,載入和顯示清單中的小區塊
無從得知Paging 3.0 以上版本透過
androidx.paging:paging-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 // ... ) }
您在資料推出後知道項目的約略大小 以非同步方式載入,最佳做法是確保項目尺寸維持 程式碼載入前後的內容相同,例如加入一些預留位置。 這有助於維持正確的捲動位置。
避免以巢狀方式嵌入可往相同方向捲動的元件
這種做法僅適用於以下情況:將可捲動子項建立巢狀結構但沒有預先定義的情況
放在另一個相同方向捲動的父項內。舉例來說
在可垂直捲動內,沒有固定高度的子項 LazyColumn
建立巢狀結構
Column
父項:
// 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 版面配置會按正常方式處理此情況,也就是排列元素 1 假裝成不同物品不過 卻無法解決
將多個元素當做某個項目的一部分發出時,系統會將這些元素視為
代表不能再個別組成如果有
元素就會在畫面上顯示,接著所有與
而且未經處理及評估如果使用這類行為,可能會對成效造成負面影響
過於頻繁。最糟糕的情況是,將所有元素放在同一個項目中
完全超過使用 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 不會嘗試在完整的
不同類型的 B 項目這麼做可以大幅提升組合的效益
重複使用 以及 Lazy 版面配置的效能
評估成效
只有在執行 版本模式,並啟用 R8 最佳化功能在偵錯版本中,Lazy 版面配置 使用者可能會比較慢捲動網頁如需更多相關資訊,請詳閱 Compose 效能。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 將
RecyclerView
遷移至 Lazy 清單 - 在 Compose 中儲存 UI 狀態
- 適用於 Jetpack Compose 的 Kotlin