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
items(), itemsIndexed(), items() với khóa để liên kết dữ liệu.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.
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 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.
// 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 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.
// 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 và RecyclerView 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ục | Hàm Composable (Kotlin DSL) | Tệp XML + ViewBinding |
| Bộ điều hợp | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| Sắp xếp/Lọc | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| Hoạt ảnh | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| Loại phần tử khác nhau | item {}/items() + when theo loại | getItemViewType + nhiều ViewHolders |
| Cuộn đến vị trí | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | Trong tiến trình (bộ đệm SubcomposeLayout) | RecycledViewPool + GapWorker |
| Mạng/Phân trang | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + 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 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).
// 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 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 định | Luô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. |
| contentType | Loại nội dung để phân tách nhóm | Chỉ đị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. |
| beyondBounds | Số màn hình prefetch | Tă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.animateItem | Hoạt ảnh thay đổi vị trí | Sử dụng cho sắp xếp và lọc có hoạt ảnh (Compose 1.7+). |
// 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
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.
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 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.
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 — 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
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.
Đọc thêm