Danh sách và lưới

Nhiều ứng dụng cần hiển thị bộ sưu tập các mục. Tài liệu này giải thích cách bạn có thể thực hiện việc này một cách hiệu quả trong Jetpack Compose.

Nếu biết rằng trường hợp sử dụng của bạn không cần phải cuộn, bạn nên sử dụng Column hoặc Row đơn giản (tuỳ thuộc vào đường đi) và phát ra nội dung của mỗi mục bằng cách lặp lại trên một danh sách theo cách sau:

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

Chúng tôi có thể làm cho Column có thể cuộn bằng cách sử dụng công cụ sửa đổi verticalScroll().

Danh sách lazy

Nếu bạn cần hiển thị một số lượng lớn các mục (hoặc một danh sách có độ dài không xác định), thì việc sử dụng bố cục như Column có thể gây ra trở ngại về hiệu suất, vì tất cả các mục sẽ được tạo và bố trí cho dù chúng có hiển thị hay không.

Compose cung cấp một tập hợp các thành phần chỉ soạn và bố trí các mục hiển thị trong khung nhìn của thành phần. Các thành phần này bao gồm LazyColumnLazyRow.

Như đã thể hiện trong tên gọi, sự khác biệt giữa LazyColumnLazyRow là hướng sắp xếp bố cục các mục và thanh cuộn. LazyColumn tạo danh sách cuộn theo chiều dọc và LazyRow tạo danh sách cuộn theo chiều ngang.

Các thành phần tải từng phần sẽ khác với hầu hết các bố cục trong công cụ Compose. Thay vì chấp nhận tham số của khối nội dung @Composable, cho phép các ứng dụng trực tiếp phát hành thành phần kết hợp, các thành phần tải từng phần sẽ cung cấp một khối LazyListScope.(). Khối LazyListScope này cung cấp một DSL, cho phép các ứng dụng mô tả nội dung của mục. Theo đó, thành phần tải từng phần sẽ chịu trách nhiệm thêm nội dung của từng mục theo yêu cầu của bố cục và vị trí cuộn.

LazyListScope DSL

DSL của LazyListScope cung cấp một số hàm để mô tả các mục trong bố cục. Về cơ bản, item() thêm một mục duy nhất và items(Int) thêm nhiều mục:

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")
    }
}

Ngoài ra, một số hàm mở rộng cho phép bạn thêm bộ sưu tập các mục, chẳng hạn như List. Các phần mở rộng này cho phép dễ dàng di chuyển mẫu Column từ bên trên:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

Ngoài ra, còn có một biến thể của hàm mở rộng items() gọi là itemsIndexed(). Hàm này cung cấp chỉ mục đó. Vui lòng xem tài liệu tham khảo LazyListScope để biết thêm thông tin chi tiết.

Lưới lazy

Các thành phần LazyVerticalGridLazyHorizontalGrid hỗ trợ việc hiển thị các mục trong lưới. Lưới dọc lazy sẽ hiển thị các mục của nó trong một vùng chứa có thể cuộn theo chiều dọc, kéo dài qua nhiều cột, trong khi các lưới ngang Lazy sẽ có cùng hành vi trên trục hoành.

Lưới có cùng khả năng API như danh sách và cũng sử dụng DSL rất giống nhau – LazyGridScope.() để mô tả nội dung.

Ảnh chụp màn hình điện thoại hiển thị một lưới ảnh

Tham số columns trong LazyVerticalGrid và tham số rows trong LazyHorizontalGrid kiểm soát cách các ô được tạo thành cột hoặc hàng. Ví dụ dưới đây hiển thị các mục trong một lưới, sử dụng GridCells.Adaptive để đặt chiều rộng tối thiểu cho mỗi cột là 128.dp:

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

LazyVerticalGrid cho phép bạn chỉ định chiều rộng của các mục để lưới có thể vừa vặn với nhiều cột nhất có thể. Chiều rộng còn lại sẽ được phân bổ đồng đều cho các cột sau khi tính toán số lượng cột. Phương pháp định cỡ thích ứng này đặc biệt hữu ích khi hiển thị các nhóm mục trên nhiều kích thước màn hình khác nhau.

Nếu biết chính xác số lượng cột cần sử dụng, bạn có thể cung cấp một phiên bản của GridCells.Fixed chứa số lượng cột bắt buộc.

Nếu thiết kế của bạn chỉ yêu cầu một số mục nhất định có kích thước không chuẩn, bạn có thể sử dụng chức năng hỗ trợ lưới để kéo dài khoảng cột tuỳ chỉnh cho các mục. Hãy chỉ định khoảng cột bằng tham số span của phương thức LazyGridScope DSLitemitems. maxLineSpan, một trong các giá trị của phạm vi khoảng, đặc biệt hữu ích khi bạn sử dụng kích thước thích ứng vì số lượng cột không cố định. Ví dụ này chỉ ra cách cung cấp khoảng hàng đầy đủ:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Lưới so le tải từng phần

LazyVerticalStaggeredGridLazyHorizontalStaggeredGrid là các thành phần kết hợp cho phép bạn tạo một lưới các mục được tải từng phần, xếp kề. Lưới dọc theo kiểu bậc thang tải từng phần hiển thị các mục trong một vùng chứa có thể cuộn theo chiều dọc, kéo dài qua nhiều cột và cho phép các mục riêng lẻ có chiều cao khác nhau. Lưới ngang tải từng phần cũng hoạt động tương tự nhưng theo chiều ngang với các mục có chiều rộng khác nhau.

Đoạn mã sau đây là ví dụ cơ bản về cách sử dụng LazyVerticalStaggeredGrid với chiều rộng 200.dp cho mỗi mục:

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()
)

Hình 1. Ví dụ về lưới dọc theo kiểu xếp kề lười

Để đặt số lượng cột cố định, bạn có thể sử dụng StaggeredGridCells.Fixed(columns) thay vì StaggeredGridCells.Adaptive. Thao tác này chia chiều rộng có sẵn cho số cột (hoặc hàng đối với lưới ngang) và mỗi mục sẽ chiếm chiều rộng (hoặc chiều cao đối với lưới ngang) đó:

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()
)
Lưới so le tải từng phần của hình ảnh trong Compose
Hình 2. Ví dụ về lưới dọc xếp kề kiểu lazy với các cột cố định

Khoảng đệm nội dung

Đôi khi, bạn cần thêm khoảng đệm quanh các cạnh của nội dung. Các thành phần tải từng phần cho phép bạn chuyển một số PaddingValues đến thông số contentPadding để hỗ trợ việc này:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

Trong ví dụ này, chúng ta thêm 16.dp khoảng đệm vào các cạnh ngang (bên trái và bên phải), sau đó thêm 8.dp vào phần đầu và cuối nội dung.

Xin lưu ý rằng khoảng đệm này áp dụng cho nội dung chứ không áp dụng cho LazyColumn. Trong ví dụ trên, mục đầu tiên sẽ thêm 8.dp khoảng đệm lên đầu, mục cuối cùng sẽ thêm 8.dp vào dưới cùng và tất cả mục sẽ có 16.dp khoảng đệm trên sang trái và phải.

Giãn cách nội dung

Để thêm khoảng cách giữa các mục, bạn có thể sử dụng Arrangement.spacedBy(). Ví dụ dưới đây sẽ thêm 4.dp khoảng cách ở giữa mỗi mục:

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Tương tự cho LazyRow:

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Tuy nhiên, lưới chấp nhận cả cách sắp xếp dọc và ngang:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Khoá mục

Theo mặc định, trạng thái của mỗi mục được khoá dựa vào vị trí của mục trong danh sách hoặc lưới. Tuy nhiên, điều này có thể gây ra sự cố nếu tập dữ liệu thay đổi, vì các mục thay đổi vị trí sẽ mất bất kỳ trạng thái nào được ghi nhớ. Nếu bạn hình dung tình huống LazyRow trong một LazyColumn, nếu hàng thay đổi vị trí mục, thì sau đó người dùng sẽ mất vị trí cuộn trong hàng.

Để xử lý điều này, bạn có thể cung cấp một khoá ổn định và duy nhất cho mỗi mục, cung cấp một khối cho thông số key. Việc cung cấp khoá ổn định cho phép trạng thái mục nhất quán qua các thay đổi của tập dữ liệu:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Bằng cách cung cấp các khoá, bạn giúp Compose xử lý việc sắp xếp lại thứ tự một cách chính xác. Ví dụ: nếu mục của bạn chứa trạng thái đã nhớ, các khoá cài đặt sẽ cho phép Compose di chuyển trạng thái này cùng với mục khi vị trí của mục thay đổi.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Tuy nhiên, có một hạn chế về những loại bạn có thể sử dụng làm khoá mục. Loại khoá này phải được hỗ trợ bởi Bundle, là cơ chế của Android để giữ lại trạng thái khi Hoạt động được tạo lại. Bundle hỗ trợ các loại như dữ liệu gốc, enum hoặc Parcelables.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Bundle phải hỗ trợ khoá này để có thể khôi phục rememberSaveable bên trong thành phần kết hợp mục khi Hoạt động được tạo lại, hoặc thậm chí khi bạn cuộn ra khỏi mục này và cuộn trở lại.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Ảnh động theo mục

Nếu đã từng sử dụng tiện ích RecyclerView, bạn sẽ biết là tiện ích đó tự động tạo ảnh động cho các thay đổi mục. Bố cục lazy cung cấp chức năng tương tự để sắp xếp lại mục. API này rất đơn giản – bạn chỉ cần đặt công cụ sửa đổi animateItem cho nội dung mục:

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

Bạn thậm chí có thể cung cấp thông số kỹ thuật về ảnh động tuỳ chỉnh, nếu bạn cần:

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

Đảm bảo bạn cung cấp khoá cho các mục của mình để có thể tìm thấy vị trí mới cho phần tử đã di chuyển.

Tiêu đề cố định (thử nghiệm)

Mẫu "tiêu đề cố định" rất hữu ích khi hiển thị danh sách dữ liệu được phân nhóm. Dưới đây là bạn có thể xem ví dụ về "danh sách liên hệ", được nhóm theo tên viết tắt của mỗi liên hệ:

Video về một chiếc điện thoại đang di chuyển lên và xuống qua một danh bạ

Để đạt được tiêu đề cố định bằng LazyColumn, bạn có thể sử dụng hàm stickyHeader() thử nghiệm, cung cấp nội dung tiêu đề như sau:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Để đạt được danh sách có nhiều tiêu đề, chẳng hạn như "danh sách liên hệ" ở trên, bạn có thể làm như sau:

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

Phản ứng với vị trí cuộn

Nhiều ứng dụng cần phản ứng và lắng nghe các thay đổi về vị trí cuộn và bố cục mục. Các thành phần tải từng phần hỗ trợ trường hợp sử dụng này bằng cách nâng cấp LazyListState:

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

Đối với các trường hợp sử dụng đơn giản, các ứng dụng thường chỉ cần biết thông tin về mục hiển thị đầu tiên. Với nội dung này, LazyListState cung cấp các thuộc tính firstVisibleItemIndexfirstVisibleItemScrollOffset.

Nếu chúng ta sử dụng ví dụ về cách hiển thị và ẩn nút dựa trên việc người dùng đã cuộn qua mục đầu tiên hay chưa:

@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()
        }
    }
}

Việc đọc trạng thái ngay trong nội dung hợp thành sẽ hữu ích khi bạn cần cập nhật các thành phần kết hợp của giao diện người dùng khác, nhưng cũng có các trường hợp mà sự kiện không cần phải xử lý trong cùng một nội dung hợp thành. Một ví dụ phổ biến về việc này là gửi một sự kiện phân tích khi người dùng cuộn qua một điểm nhất định. Để xử lý việc này một cách hiệu quả, bạn có thể sử dụng snapshotFlow():

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState cũng cung cấp thông tin về tất cả các mục hiện đang được hiển thị và giới hạn của các mục đó trên màn hình, thông qua thuộc tính layoutInfo. Hãy xem lớp LazyListLayoutInfo để biết thêm thông tin.

Kiểm soát vị trí cuộn

Bên cạnh việc phản ứng với vị trí cuộn, việc ứng dụng có thể kiểm soát vị trí cuộn cũng hữu ích. LazyListState hỗ trợ nội dung này thông qua hàm scrollToItem(). Hành động hỗ trợ này 'ngay lập tức' sẽ ghim vị trí cuộn và animateScrollToItem() cuộn bằng cách sử dụng ảnh động (còn gọi là cuộn mượt):

@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)
            }
        }
    )
}

Các tập dữ liệu lớn (phân trang)

Thư viện Paging cho phép các ứng dụng hỗ trợ danh sách lớn các mục, tải và hiển thị nhiều danh sách nhỏ trong danh sách nếu cần. Paging phiên bản 3.0 trở lên cung cấp dịch vụ hỗ trợ sử dụng công cụ Compose thông qua thư viện androidx.paging:paging-compose.

Để hiển thị danh sách nội dung được phân trang, chúng ta có thể sử dụng hàm mở rộng collectAsLazyPagingItems(), sau đó chuyển LazyPagingItems trả về vào items() trong LazyColumn của mình. Tương tự như hỗ trợ Paging trong các chế độ xem, bạn có thể hiển thị các trình giữ chỗ trong khi dữ liệu tải bằng cách kiểm tra xem item có phải là null không:

@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()
            }
        }
    }
}

Mẹo sử dụng bố cục Lazy

Bạn có thể tham khảo một số mẹo để đảm bảo bố cục Lazy hoạt động như dự kiến.

Tránh sử dụng các mục có kích thước 0 pixel

Điều này có thể xảy ra trong những trường hợp như khi bạn dự kiến truy xuất không đồng bộ một số dữ liệu như hình ảnh, để lấp đầy các mục trong danh sách ở giai đoạn sau. Điều đó sẽ khiến bố cục Lazy sắp xếp tất cả các mục của nó trong lần đo đầu tiên, vì chiều cao của chúng là 0 pixel và có thể vừa với tất cả các mục trong khung nhìn. Khi các mục đã tải và chiều cao của chúng được mở rộng, các bố cục Lazy sẽ loại bỏ những mục khác được tạo một cách không cần thiết trong lần đầu tiên, vì thực tế thì chúng không thể vừa với khung nhìn. Để tránh trường hợp này, bạn nên đặt kích thước mặc định cho các mục của mình, để bố cục Lazy có thể thực hiện tính toán chính xác số lượng mục thực sự có thể vừa với khung nhìn:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Khi biết kích thước gần đúng của các mục sau khi tải dữ liệu không đồng bộ, bạn nên đảm bảo kích thước của các mục không thay đổi trước và sau khi tải, bằng cách thêm vào một số phần giữ chỗ. Việc này giúp duy trì vị trí cuộn chính xác.

Tránh lồng các thành phần có thể cuộn theo cùng một hướng

Điều này chỉ áp dụng cho những trường hợp khi lồng các con có thể cuộn được mà không có kích thước xác định trước bên trong cha mẹ có thể cuộn theo cùng một hướng khác. Chẳng hạn cố gắng lồng một thành phần con LazyColumn không có chiều cao cố định trong thành phần mẹ Column có thể cuộn được theo chiều dọc:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Thay vào đó, bạn có thể đạt được kết quả tương tự bằng cách gói tất cả các thành phần kết hợp bên trong một LazyColumn mẹ và sử dụng DSL của thành phần đó để truyền các loại nội dung khác nhau. Điều này cho phép tạo ra các mục riêng lẻ, cũng như nhiều mục danh sách ở cùng một nơi:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Vui lòng lưu ý trong trường hợp bạn lồng những bố cục có hướng khác nhau, chẳng hạn như chế độ mẹ có thể cuộn được Row và con LazyColumn, được phép:

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

Cũng như các trường hợp bạn vẫn sử dụng bố cục cùng hướng nhưng đồng thời đặt kích thước cố định cho các thành phần con lồng nhau:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Cần cảnh giác khi đặt nhiều phần tử vào một mục

Ở ví dụ này, mục thứ hai chứa hàm lambda xuất ra 2 mục trong một khối:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

Bố cục lazy sẽ xử lý điều này như mong đợi - chúng sẽ bố trí các phần tử lần lượt như thể chúng là các mục khác nhau. Tuy nhiên, cũng có một vài bất cập khi làm như vậy.

Khi nhiều phần tử được xuất ra dưới dạng một phần của một mục, các phần tử đó sẽ được xử lý như một thực thể, nghĩa là không thể tạo riêng từng phần tử được nữa. Nếu một phần tử hiển thị trên màn hình, thì tất cả các phần tử tương ứng với mục đó phải được sắp xếp và đo lường. Điều này có thể ảnh hưởng đến hiệu suất nếu sử dụng quá mức. Trong trường hợp đặc biệt khi đặt tất cả các phần tử vào một mục, mục đó sẽ hoàn toàn đánh bại mục đích sử dụng bố cục Lazy. Ngoài những bất cập tiềm ẩn về hiệu suất, việc đặt thêm nhiều phần tử vào cùng một mục cũng sẽ ảnh hưởng đến scrollToItem()animateScrollToItem().

Tuy nhiên, có những trường hợp sử dụng hợp lệ cho việc đưa nhiều phần tử vào một mục, chẳng hạn như bộ chia bên trong một danh sách. Bạn không muốn bộ chia thay đổi các chỉ số cuộn, vì chúng không được coi là các phần tử độc lập. Ngoài ra, hiệu suất sẽ không bị ảnh hưởng vì các bộ chia nhỏ. Một bộ chia có thể sẽ cần được hiển thị khi mục trước nó hiển thị, để bộ chia có thể nằm trong mục trước đó:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Cân nhắc sử dụng cách sắp xếp tuỳ chỉnh

Thông thường, danh sách Lazy có nhiều mục và các mục này chiếm nhiều hơn kích thước của vùng chứa cuộn. Tuy nhiên, khi danh sách của bạn có ít mục, bạn có thể thiết kế các yêu cầu cụ thể hơn về cách đặt những mục này trong khung nhìn.

Để đạt được điều này, bạn có thể sử dụng Arrangement tuỳ chỉnh theo chiều dọc và truyền tham số này đến LazyColumn. Trong ví dụ sau, đối tượng TopWithFooter chỉ cần triển khai phương thức arrange. Đầu tiên, nó sẽ định vị lần lượt các mục. Tiếp theo, nếu tổng chiều cao đã sử dụng thấp hơn chiều cao khung nhìn, thì chân trang sẽ được đặt ở dưới cùng:

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()
        }
    }
}

Hãy cân nhắc việc thêm contentType

Bắt đầu bằng Compose 1.2, để tăng tối đa hiệu suất của bố cục Lazy, hãy cân nhắc thêm contentType vào danh sách hoặc lưới của bạn. Điều này cho phép bạn chỉ định loại nội dung cho mỗi mục của bố cục, trong trường hợp bạn đang soạn danh sách hoặc lưới bao gồm nhiều loại mục khác nhau:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

Khi bạn cung cấp contentType, Compose chỉ có thể sử dụng lại thành phần giữa các mục cùng loại. Vì việc tái sử dụng sẽ hiệu quả hơn khi bạn tạo các mục có cấu trúc tương tự nhau, bạn nên cung cấp loại nội dung nhằm đảm bảo Compose không cố soạn một mục loại A ở đầu một mục hoàn toàn khác loại B. Điều này giúp tối đa hoá lợi ích của việc sử dụng lại thành phần cũng như hiệu suất bố cục Lazy của bạn.

Đo lường hiệu suất

Bạn chỉ có thể đo lường hiệu suất của bố cục Lazy khi chạy ở chế độ phát hành và bật tính năng tối ưu hoá R8. Trên các bản dựng gỡ lỗi, thao tác cuộn bố cục Lazy có thể xuất hiện chậm hơn. Để biết thêm thông tin chi tiết về vấn đề này, hãy đọc bài viết Hiệu suất của Compose.