Danh sách

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 như 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(). Hãy xem tài liệu về Cử chỉ để biết thêm thông tin.

Các thành phần kết hợp tải lười

Nếu bạn cần hiển thị một số lượng lớn 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 các vấn đề về hiệu suất, vì tất cả các mục sẽ được tạo và bố trí cho dù các mục đó 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 lười 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 thông số khối nội dung @Composable, cho phép các ứng dụng trực tiếp phát hành các thành phần kết hợp, các thành phần tải lười 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. Sau đó, thành phần tải lười 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

@Composable
fun MessageList(messages: List<Message>) {
    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.

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 lười 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),
) {
    // ...
}

Ảnh động theo mục

Nếu đã sử dụng tiện ích RecyclerView, bạn sẽ biết rằng tiện ích đó tự động thay đổi mục. Bố cục tải lười chưa cung cấp chức năng đó. Điều đó có nghĩa là các thay đổi về mục sẽ tạo ra một 'bẫy' ngay lập tức. Bạn có thể theo dõi lỗi này để theo dõi mọi thay đổi đối với tính năng này.

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 đề, như ví dụ về "danh sách liên hệ" ở trên, bạn có thể làm như sau:

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

Lưới (thử nghiệm)

Thành phần kết hợp LazyVerticalGrid hỗ trợ thử nghiệm để hiển thị các mục trong lưới.

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

Thông số cells kiểm soát cách các ô được sắp xếp tạo thành cột. Ví dụ sau hiển thị các mục trong một lưới, sử dụng GridCells.Adaptive để đặt chiều rộng cho mỗi cột có độ rộng ít nhất là 128.dp:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        cells = GridCells.Adaptive(minSize = 128.dp)
    ) {
        items(photos) { photo ->
            PhotoItem(photo)
        }
    }
}

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.

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 lười 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:

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

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 == true }
        .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:

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

Khoá mục

Theo mặc định, trạng thái của mỗi mục là quan trọng nhất so với vị trí của mục trong danh sách. 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í hiệu quả 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 khóa ổ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 khóa ổn định cho phép trạng thái mục nhất quán khi các thay đổi của tập dữ liệu:

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