Skip to Content

Lists và Grids trong Jetpack Compose

Hiển thị danh sách và lưới dữ liệu là nhu cầu phổ biến trong ứng dụng. Compose cung cấp các Lazy composables hiệu quả cho việc này.

1. LazyColumn - Danh sách cuộn dọc

Cơ bản

@Composable fun BasicLazyColumn() { LazyColumn { items(100) { index -> Text( text = "Item $index", modifier = Modifier .fillMaxWidth() .padding(16.dp) ) } } }

Với danh sách items

@Composable fun ProductList(products: List<Product>) { LazyColumn( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { items( items = products, key = { it.id } // Important: unique key cho mỗi item ) { product -> ProductCard(product) } } }

Mixed content

@Composable fun MixedContent() { LazyColumn { item { Text("Header", style = MaterialTheme.typography.headlineMedium) } items(10) { index -> Text("Item $index") } item { Text("Section 2", style = MaterialTheme.typography.titleLarge) } items(5) { index -> Text("Section 2 Item $index") } item { Text("Footer") } } }

2. LazyRow - Danh sách cuộn ngang

@Composable fun HorizontalCarousel(items: List<CarouselItem>) { LazyRow( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(items, key = { it.id }) { item -> Card( modifier = Modifier.size(width = 150.dp, height = 200.dp) ) { Image( painter = painterResource(item.imageRes), contentDescription = item.title, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } } } }

3. LazyVerticalGrid - Lưới cuộn dọc

Fixed columns

@Composable fun PhotoGrid(photos: List<Photo>) { LazyVerticalGrid( columns = GridCells.Fixed(3), // 3 cột cố định contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(photos, key = { it.id }) { photo -> AsyncImage( model = photo.url, contentDescription = null, modifier = Modifier.aspectRatio(1f), contentScale = ContentScale.Crop ) } } }

Adaptive columns

@Composable fun AdaptiveGrid(items: List<Item>) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp), // Tự điều chỉnh số cột contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(items, key = { it.id }) { item -> ItemCard(item) } } }

4. LazyVerticalStaggeredGrid - Lưới so le

@Composable fun PinterestStyleGrid(items: List<PinItem>) { LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(2), contentPadding = PaddingValues(8.dp), verticalItemSpacing = 8.dp, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(items, key = { it.id }) { item -> PinCard( item = item, modifier = Modifier.fillMaxWidth() // Height tự động theo content ) } } } @Composable fun PinCard(item: PinItem, modifier: Modifier = Modifier) { Card(modifier = modifier) { Column { AsyncImage( model = item.imageUrl, contentDescription = null, modifier = Modifier .fillMaxWidth() .aspectRatio(item.aspectRatio) // Mỗi item có ratio khác nhau ) Text(item.title, modifier = Modifier.padding(8.dp)) } } }

5. Key và ContentType - Performance

Sử dụng key

Key giúp Compose identify items đúng khi list thay đổi:

@Composable fun KeyExample(items: List<Item>) { LazyColumn { items( items = items, key = { it.id } // Unique, stable ID ) { item -> ItemRow(item) } } }

Sử dụng contentType

contentType giúp optimize composition khi có nhiều loại items:

@Composable fun MixedList(feed: List<FeedItem>) { LazyColumn { items( items = feed, key = { it.id }, contentType = { item -> when (item) { is FeedItem.Header -> "header" is FeedItem.Post -> "post" is FeedItem.Ad -> "ad" } } ) { item -> when (item) { is FeedItem.Header -> HeaderCard(item) is FeedItem.Post -> PostCard(item) is FeedItem.Ad -> AdCard(item) } } } }

6. Sticky Headers

@OptIn(ExperimentalFoundationApi::class) @Composable fun ContactList(contacts: Map<Char, List<Contact>>) { LazyColumn { contacts.forEach { (letter, contactsForLetter) -> stickyHeader { Text( text = letter.toString(), modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceVariant) .padding(16.dp), style = MaterialTheme.typography.titleMedium ) } items(contactsForLetter, key = { it.id }) { contact -> ContactRow(contact) } } } }

7. Scroll State và Control

Observing Scroll Position

@Composable fun ScrollAwareList() { val listState = rememberLazyListState() // Kiểm tra có scroll được không val isScrolled = listState.firstVisibleItemIndex > 0 // Observe scroll position LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .collect { index -> println("First visible item: $index") } } LazyColumn(state = listState) { items(100) { index -> Text("Item $index") } } }

Programmatic Scrolling

@Composable fun ScrollToTopExample() { val listState = rememberLazyListState() val scope = rememberCoroutineScope() val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } Box { LazyColumn(state = listState) { items(100) { Text("Item $it") } } if (showButton) { FloatingActionButton( onClick = { scope.launch { listState.animateScrollToItem(0) } }, modifier = Modifier .align(Alignment.BottomEnd) .padding(16.dp) ) { Icon(Icons.Default.KeyboardArrowUp, null) } } } }

8. Pull to Refresh

@OptIn(ExperimentalMaterial3Api::class) @Composable fun RefreshableList( items: List<Item>, onRefresh: suspend () -> Unit ) { val refreshState = rememberPullToRefreshState() var isRefreshing by remember { mutableStateOf(false) } if (refreshState.isRefreshing) { LaunchedEffect(true) { isRefreshing = true onRefresh() isRefreshing = false refreshState.endRefresh() } } Box( modifier = Modifier .fillMaxSize() .nestedScroll(refreshState.nestedScrollConnection) ) { LazyColumn { items(items) { item -> ItemRow(item) } } PullToRefreshContainer( state = refreshState, modifier = Modifier.align(Alignment.TopCenter) ) } }

9. Paging với Compose

Setup

// build.gradle.kts dependencies { implementation("androidx.paging:paging-compose:3.3.0") }

Sử dụng

@Composable fun PagedList(viewModel: MyViewModel) { val lazyPagingItems = viewModel.pagingData.collectAsLazyPagingItems() LazyColumn { items( count = lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it.id } ) { index -> val item = lazyPagingItems[index] item?.let { ItemRow(it) } } // Loading indicator when (lazyPagingItems.loadState.append) { is LoadState.Loading -> { item { CircularProgressIndicator( modifier = Modifier .fillMaxWidth() .padding(16.dp) .wrapContentWidth(Alignment.CenterHorizontally) ) } } is LoadState.Error -> { item { Button(onClick = { lazyPagingItems.retry() }) { Text("Retry") } } } else -> {} } } }

10. Item Animations

AnimatedVisibility cho items

@Composable fun AnimatedItemList(items: List<Item>) { LazyColumn { items(items, key = { it.id }) { item -> var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { visible = true } AnimatedVisibility( visible = visible, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically() ) { ItemRow(item) } } } }

Swipe to Dismiss

@OptIn(ExperimentalMaterial3Api::class) @Composable fun SwipeableItem( item: Item, onDismiss: () -> Unit ) { val dismissState = rememberSwipeToDismissBoxState( confirmValueChange = { if (it == SwipeToDismissBoxValue.EndToStart) { onDismiss() true } else { false } } ) SwipeToDismissBox( state = dismissState, backgroundContent = { Box( modifier = Modifier .fillMaxSize() .background(Color.Red) .padding(16.dp), contentAlignment = Alignment.CenterEnd ) { Icon(Icons.Default.Delete, null, tint = Color.White) } } ) { ItemCard(item) } }

📝 Tóm tắt

ComponentMục đích
LazyColumnDanh sách cuộn dọc
LazyRowDanh sách cuộn ngang
LazyVerticalGridLưới cuộn dọc
LazyHorizontalGridLưới cuộn ngang
LazyVerticalStaggeredGridLưới so le (Pinterest style)

Best Practices

  • Luôn sử dụng key với unique, stable ID
  • Dùng contentType khi có nhiều loại items
  • Tránh làm composable trong items quá nặng
  • Sử dụng derivedStateOf cho derived scroll values
  • Dùng Paging 3 cho large datasets
Last updated on