LazyColumn — nó là gì, danh sách hiệu quả trong Compose

Tác giả: IT Sectr Đã đăng: 2026-02-24 Thời gian đọc: 10 phút

LazyColumn — một thành phần Jetpack Compose để hiển thị danh sách cuộn với tải lười. Không giống RecyclerView, LazyColumn không tạo ViewHolders và không sử dụng XML — tất cả các phần tử được mô tả khai báo thông qua các hàm composable. Chúng tôi giới thiệu LazyColumn là gì, LazyListState và API items hoạt động như thế nào, và cách đạt hiệu suất 60 FPS. Tại IT Sectr, chúng tôi sử dụng LazyColumn trong tất cả các dự án Compose mới. Để so sánh với cách tiếp cận cổ điển, hãy đọc bài viết về RecyclerView.

Những điểm chính

  • LazyColumn — một thành phần Jetpack Compose cho danh sách dọc với tải lười, một phần của Material Design 3.
  • LazyListState — trạng thái danh sách: vị trí cuộn, phần tử hiển thị, cuộn lập trình.
  • items API — các hàm items(), itemsIndexed(), items() với khóa để liên kết dữ liệu.
  • key — tham số để nhận dạng phần tử ổn định, cho phép Compose tái sử dụng thành phần.
  • LazyVerticalGrid — phiên bản LazyColumn cho lưới với số cột cố định.

LazyColumn trong Jetpack Compose là gì?

LazyColumn là một hàm composable từ thư viện Foundation Compose hiển thị danh sách dọc các phần tử với tải lười: chỉ các phần tử hiển thị trên màn hình hoặc gần đó (vùng prefetch) được tạo và kết hợp. LazyColumn xuất hiện trong Compose 1.0 (2021) cùng với LazyRow (danh sách ngang) và LazyVerticalGrid (lưới).

Tải lười hoạt động thông qua cơ chế SubcomposeLayout: LazyColumn đo không gian khả dụng, yêu cầu phạm vi hiển thị từ LayoutInfo và chỉ kết hợp các phần tử trong phạm vi đó. Các phần tử cuộn ra khỏi màn hình rời khỏi thành phần (ngoại trừ bộ đệm prefetch). Theo Google (Android Performance, 2026), LazyColumn duy trì 60 FPS khi cuộn qua danh sách 10000+ phần tử trên các thiết bị tầm trung.

Khác biệt với Column + ScrollState

Column với cuộn dọc là sự sắp xếp bình thường của tất cả các phần tử nối tiếp nhau với bộ điều chỉnh verticalScroll được áp dụng. Column không lười: tất cả các phần tử con được kết hợp ngay lập tức, ngay cả khi chúng không hiển thị. Đối với danh sách 200+ phần tử, Column gây ra độ trễ đáng kể khi khởi động. LazyColumn chỉ kết hợp các phần tử hiển thị + bộ đệm (mặc định 1 màn hình tiến/lùi), khiến nó không thể thiếu cho danh sách dài.

items API: liên kết dữ liệu với danh sách

items API là một tập hợp các hàm mở rộng cho LazyColumn chấp nhận danh sách dữ liệu và bộ mô tả DSL cho mỗi phần tử. Các hàm chính là: items(count, key, itemContent) cho số lượng cố định, items(list, key, itemContent) cho danh sách, itemsIndexed(list, key, itemContent) với chỉ mục. Tham số key là bắt buộc cho hiệu suất — nó cung cấp cho Compose một định danh phần tử ổn định.

kotlin
// LazyColumn cơ bản với itemContent
@Composable
fun ArticleList(articles: List<Article>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header — không lười, luôn ở đầu
        item {
            Text("Bài viết mới nhất", style = MaterialTheme.typography.headlineMedium)
        }

        // Danh sách bài viết với khóa theo id
        items(articles, key = { it.id }) { article ->
            ArticleCard(
                title = article.title,
                summary = article.summary,
                onClick = { onArticleClick(article.id) }
            )
        }

        // Footer với chỉ báo tải
        item {
            CircularProgressIndicator(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            )
        }
    }
}

// Phần tử danh sách tùy chỉnh
@Composable
fun ArticleCard(title: String, summary: String, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
    ) {
        Column(modifier = Modifier.padding(12.dp)) {
            Text(text = title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = summary, style = MaterialTheme.typography.bodyMedium)
        }
    }
}

item {} vs items(): sử dụng item {} cho các phần tử đơn lẻ luôn có trong danh sách (tiêu đề, chân trang, dấu phân cách). Sử dụng items() cho danh sách dữ liệu động. items() chấp nhận Iterable hoặc Int và một khối DSL được gọi cho mỗi phần tử. Kết hợp item và items trong một LazyColumn: thứ tự gọi xác định thứ tự hiển thị.

LazyListState: quản lý vị trí cuộn

LazyListState là một đối tượng lưu trữ trạng thái danh sách: vị trí cuộn hiện tại (firstVisibleItemIndex, firstVisibleItemScrollOffset), các phần tử hiển thị (layoutInfo) và các phương thức cuộn lập trình (scrollToItem, animateScrollToItem). LazyListState được tạo qua rememberLazyListState() và được truyền vào LazyColumn qua tham số state.

kotlin
// LazyColumn với bảo toàn vị trí cuộn
@Composable
fun ScrollingList(items: List<Item>) {
    val listState = rememberLazyListState()

    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn(state = listState) {
            items(items, key = { it.id }) { item ->
                ListItemView(item = item)
            }
        }

        // Nút "Lên" xuất hiện sau phần tử thứ 10
        val showButton by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 10 }
        }

        AnimatedVisibility(visible = showButton) {
            FloatingActionButton(
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(16.dp),
                onClick = {
                    coroutineScope.launch {
                        listState.animateScrollToItem(0)
                    }
                }
            ) {
                Icon(Icons.Default.KeyboardArrowUp, "Lên đầu")
            }
        }
    }
}

// Lấy thông tin phần tử hiển thị
@Composable
fun ListDebugInfo(listState: LazyListState) {
    val visibleItems = listState.layoutInfo.visibleItemsInfo
    val totalItems = listState.layoutInfo.totalItemsCount
    val firstItem = visibleItems.firstOrNull()?.index
    val lastItem = visibleItems.lastOrNull()?.index

    Text("Hiển thị $firstItem–$lastItem trong $totalItems")
}

Cuộn lập trình được thực hiện qua các hàm suspend của listState: scrollToItem(index, scrollOffset) — di chuyển tức thời (không có hoạt ảnh), animateScrollToItem(index) — có hoạt ảnh. Để cuộn từ coroutine, sử dụng LaunchedEffect hoặc coroutineScope.launch. Quan trọng: không gọi hàm suspend từ thành phần (bên trong @Composable) — sử dụng lambda (onClick, LaunchedEffect).

LazyColumn vs RecyclerView: so sánh cách tiếp cận

LazyColumnRecyclerView giải quyết cùng một vấn đề — hiển thị hiệu quả danh sách dài. Sự khác biệt nằm ở kiến trúc: RecyclerView sử dụng ViewHolder và Adapter (View Android cổ điển), LazyColumn sử dụng các hàm composable khai báo không cần XML hay ViewHolder. Lựa chọn phụ thuộc vào công nghệ của dự án: Compose vs UI dựa trên View.

Tham sốLazyColumn (Compose)RecyclerView (View)
Bố cụcHàm Composable (Kotlin DSL)Tệp XML + ViewBinding
Bộ điều hợpitems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
Sắp xếp/LọcSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
Hoạt ảnhAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
Loại phần tử khác nhauitem {}/items() + when theo loạigetItemViewType + nhiều ViewHolders
Cuộn đến vị trílistState.animateScrollToItem()layoutManager.scrollToPosition()
PrefetchTrong tiến trình (bộ đệm SubcomposeLayout)RecycledViewPool + GapWorker
Mạng/Phân trangcollectAsLazyPagingItems() + Paging 3PagingDataAdapter + Paging 3

Khuyến nghị của Google (Android Developers, 2026): cho các dự án mới trên Jetpack Compose, hãy sử dụng LazyColumn. Cho các dự án dựa trên View, hãy sử dụng RecyclerView. LazyColumn yêu cầu Compose, thêm khoảng 3–5 MB vào APK — hãy cân nhắc điều này khi hỗ trợ thiết bị cũ. Nếu dự án đã sử dụng hệ thống View, LazyColumn không thể cùng tồn tại trên một màn hình với bố cục XML mà không có AndroidView — trong những trường hợp như vậy, tốt hơn nên tiếp tục dùng RecyclerView.

LazyVerticalGrid và LazyHorizontalGrid: lưới

LazyVerticalGrid là phiên bản của LazyColumn cho lưới với số cột cố định. LazyHorizontalGrid là lưới ngang với số hàng cố định. Cả hai thành phần đều sử dụng nguyên tắc tải lười và cùng API items như LazyColumn. Tham số columns xác định số cột qua GridCells.Fixed(N) hoặc GridCells.Adaptive(minSize).

kotlin
// Lưới với cột thích ứng
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 128.dp),
        contentPadding = PaddingValues(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(photos, key = { it.id }) { photo ->
            AsyncImage(
                model = photo.url,
                contentDescription = photo.title,
                modifier = Modifier.aspectRatio(1f)
            )
        }
    }
}

// Danh sách ngang (LazyRow)
@Composable
fun CategoryCarousel(categories: List<Category>) {
    LazyRow(
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(categories, key = { it.id }) { category ->
            FilterChip(
                selected = category.isSelected,
                onClick = { onCategoryClick(category.id) },
                label = { Text(category.name) }
            )
        }
    }
}

GridCells.Adaptive tự động tính số cột dựa trên kích thước ô tối thiểu. Ví dụ, GridCells.Adaptive(128.dp) trên màn hình rộng 360 dp đặt 2 cột, trên máy tính bảng 600 dp — 4 cột. GridCells.Fixed(2) luôn hiển thị chính xác 2 cột. Adaptive được ưa chuộng cho thích ứng đa màn hình — nó tự động điều chỉnh theo chiều rộng màn hình mà không cần truy vấn media.

Hiệu suất LazyColumn: key, contentType, prefetch

Hiệu suất của LazyColumn phụ thuộc vào ba yếu tố: độ ổn định của khóa (key), loại nội dung (contentType) và bộ đệm prefetch (beyondBounds). Theo mặc định, LazyColumn đệm 1 màn hình tiến và 1 lùi. Google (Android Developers, 2026) khuyến nghị cấu hình các tham số này cho danh sách có loại phần tử khác nhau hoặc thành phần phức tạp.

Tham sốMô tảKhuyến nghị
keyĐịnh danh phần tử ổn địnhLuôn chỉ định key = { it.id }. Không có key, Compose sử dụng chỉ mục và việc sắp xếp lại phần tử làm hỏng hoạt ảnh.
contentTypeLoại nội dung để phân tách nhómChỉ định cho danh sách với loại phần tử khác nhau (văn bản + hình ảnh + quảng cáo). Compose sẽ chỉ tái sử dụng thành phần trong cùng contentType.
beyondBoundsSố màn hình prefetchTăng lên 2–3 cho danh sách có hình ảnh nặng. Mặc định là 1 màn hình.
Modifier.animateItemHoạt ảnh thay đổi vị tríSử dụng cho sắp xếp và lọc có hoạt ảnh (Compose 1.7+).
kotlin
// LazyColumn tối ưu với contentType và prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        modifier = Modifier.fillMaxSize(),
        beyondBoundsPageCount = 2  // prefetch 2 màn hình
    ) {
        items(
            items = items,
            key = { it.id },
            contentType = { it.type }  // phân chia theo loại
        ) { item ->
            when (item) {
                is FeedItem.Post -> PostView(item)
                is FeedItem.Ad -> AdView(item)
                is FeedItem.Suggested -> SuggestedView(item)
            }
        }
    }
}

// Đo hiệu suất qua listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
    val scrollInfo = listState.layoutInfo
    val visibleCount = scrollInfo.visibleItemsInfo.size
    val total = scrollInfo.totalItemsCount

    // derivedStateOf — không kết hợp lại nếu giá trị không thay đổi
    val scrollProgress by remember {
        derivedStateOf {
            if (total > 0) visibleCount.toFloat() / total else 0f
        }
    }

    LinearProgressIndicator(
        progress = scrollProgress,
        modifier = Modifier.fillMaxWidth().height(2.dp)
    )
}

Lỗi thường gặp: (1) thiếu key — các phần tử được kết hợp lại khi danh sách thay đổi; (2) sử dụng mutableStateListOf không có luồng snapshot — các thay đổi có thể không được theo dõi; (3) gọi hàm suspend bên trong itemContent — làm gián đoạn thành phần; (4) không có contentType cho loại hỗn hợp — Compose tái sử dụng thành phần quảng cáo cho bài đăng văn bản, gây ra hiện tượng giả tạo thị giác. Tại IT Sectr, chúng tôi thêm contentType vào tất cả danh sách có ba loại nội dung trở lên.

Câu hỏi thường gặp

LazyColumn cuộn chậm — phải làm sao?

Kiểm tra ba tham số: (1) key — không có khóa ổn định, Compose không thể tái sử dụng thành phần, (2) contentType — cho danh sách có loại phần tử khác nhau, (3) beyondBoundsPageCount — tăng lên 2 cho hình ảnh. Di chuyển tính toán nặng bên trong itemContent vào remember với khóa theo dữ liệu. Sử dụng Modifier.drawWithContent thay cho Image cho đồ họa đơn giản.

Làm thế nào để thêm dấu phân cách giữa các phần tử LazyColumn?

Sử dụng LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) cho khoảng cách giữa các phần tử. Đối với dấu phân cách trực quan có đường kẻ, thêm item { Divider() } giữa các items. Dấu phân cách tự động: items(items, key = { it.id }) { item -> ... } và giữa mỗi phần tử chèn item { HorizontalDivider() }. Để làm điều này, sử dụng LazyListScope.items(list, key) { /* phần tử */ } và riêng biệt LazyListScope.item { Divider() }.

LazyColumn bên trong Column không hoạt động — tại sao?

LazyColumn có chiều cao vô hạn (fillMaxSize) theo mặc định. Bên trong Column không có chiều cao cố định, LazyColumn không thể xác định kích thước của nó. Giải pháp: (1) đặt chiều cao cố định cho LazyColumn: Modifier.height(400.dp), (2) sử dụng Modifier.weight(1f) bên trong Column, (3) không lồng LazyColumn bên trong vùng chứa cuộn dọc — sử dụng LazyColumn làm phần tử gốc với item {} cho tiêu đề và chân trang.

Làm thế nào để cập nhật LazyColumn khi dữ liệu thay đổi?

LazyColumn tự động phản ứng với thay đổi State. Nếu dữ liệu được lưu trữ trong mutableStateListOf hoặc mutableStateOf, Compose chỉ kết hợp lại các phần tử đã thay đổi. Đối với danh sách từ ViewModel, sử dụng collectAsState() với Flow. Khi cập nhật danh sách với key, Compose tự động tạo hoạt ảnh cho các thay đổi (thêm, xóa, sắp xếp lại) qua Modifier.animateItemPlacement().

LazyColumn vs LazyVerticalGrid — chọn cái nào?

LazyColumn — cho danh sách một cột (feed, chat, bình luận). LazyVerticalGrid — cho lưới (thư viện ảnh, danh mục, biểu tượng). Nếu các phần tử nên ở một cột trên điện thoại và hai cột trên máy tính bảng — sử dụng LazyVerticalGrid với GridCells.Adaptive. Nếu cần lưới hỗn hợp (số cột khác nhau trong một phần) — sử dụng LazyColumn và chèn LazyRow ngang hoặc LazyVerticalGrid lồng bên trong items.

Tổng kết

  • LazyColumn — một thành phần Jetpack Compose cho danh sách dọc lười với API khai báo.
  • items() API chấp nhận danh sách và khối DSL cho mỗi phần tử; key bắt buộc cho hiệu suất.
  • LazyListState quản lý vị trí cuộn, cung cấp hàm suspend cho cuộn lập trình.
  • contentType phân tách nhóm thành phần cho danh sách có loại phần tử khác nhau.
  • LazyVerticalGrid / LazyRow — biến thể cho lưới và danh sách ngang với cùng API items.
  • LazyColumn duy trì 60 FPS cho 10000+ phần tử nhờ SubcomposeLayout và bộ đệm prefetch.
  • Cho dự án Compose mới sử dụng LazyColumn; cho dự án dựa trên View — RecyclerView.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm