清單和格線

許多應用程式都需要顯示項目集合。本文件將說明 這在 Jetpack Compose 中能有效率地完成

如果您知道您的應用程式將不需要捲動任何頁面,建議您 使用簡單的 ColumnRow (視方向而定),並由以下做法發出各個項目的內容: 以下列方式疊代清單:

@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 參數: LazyVerticalGridrows 參數 LazyHorizontalGrid 控制儲存格轉換為欄或列的方式。下列 範例會使用 GridCells.Adaptive敬上 將每欄的寬度設為至少 128.dp

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid 可讓您指定項目寬度,然後格線會 盡可能容納更多欄剩餘的寬度會均等分配 資料欄數。 這種自動調整尺寸的方式特別適合用來顯示項目組合 支援各種螢幕大小

如果您知道要使用的確切欄數,可以改為提供 執行個體 GridCells.Fixed敬上 。

如果設計只有某些項目需要非標準尺寸 您可以使用格狀支援功能,為項目提供自訂欄時距。 請使用以下項目的 span 參數指定資料欄時距: LazyGridScope DSL itemitems 方法。 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()
)

圖 1.延遲交錯垂直格線範例

如要設定固定數量的欄,請使用 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 中延遲交錯圖片格線
圖 2.採用固定欄的延遲交錯垂直格線範例

內容間距

有時您必須在內容邊緣周圍加上邊框間距。懶人 可讓您傳送 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 效能