Lists và Grids
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
Phần tiêu đề “1. LazyColumn - Danh sách cuộn dọc”Cơ bản
Phần tiêu đề “Cơ bản”@Composablefun BasicLazyColumn() { LazyColumn { items(100) { index -> Text( text = "Item $index", modifier = Modifier .fillMaxWidth() .padding(16.dp) ) } }}Với danh sách items
Phần tiêu đề “Với danh sách items”@Composablefun 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
Phần tiêu đề “Mixed content”@Composablefun 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
Phần tiêu đề “2. LazyRow - Danh sách cuộn ngang”@Composablefun 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
Phần tiêu đề “3. LazyVerticalGrid - Lưới cuộn dọc”Fixed columns
Phần tiêu đề “Fixed columns”@Composablefun 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
Phần tiêu đề “Adaptive columns”@Composablefun 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
Phần tiêu đề “4. LazyVerticalStaggeredGrid - Lưới so le”@Composablefun 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 ) } }}
@Composablefun 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
Phần tiêu đề “5. Key và ContentType - Performance”Sử dụng key
Phần tiêu đề “Sử dụng key”Key giúp Compose identify items đúng khi list thay đổi:
@Composablefun KeyExample(items: List<Item>) { LazyColumn { items( items = items, key = { it.id } // Unique, stable ID ) { item -> ItemRow(item) } }}Sử dụng contentType
Phần tiêu đề “Sử dụng contentType”contentType giúp optimize composition khi có nhiều loại items:
@Composablefun 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
Phần tiêu đề “6. Sticky Headers”@OptIn(ExperimentalFoundationApi::class)@Composablefun 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
Phần tiêu đề “7. Scroll State và Control”Observing Scroll Position
Phần tiêu đề “Observing Scroll Position”@Composablefun 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
Phần tiêu đề “Programmatic Scrolling”@Composablefun 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
Phần tiêu đề “8. Pull to Refresh”@OptIn(ExperimentalMaterial3Api::class)@Composablefun 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
Phần tiêu đề “9. Paging với Compose”Setup
Phần tiêu đề “Setup”dependencies { implementation("androidx.paging:paging-compose:3.3.0")}Sử dụng
Phần tiêu đề “Sử dụng”@Composablefun 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
Phần tiêu đề “10. Item Animations”AnimatedVisibility cho items
Phần tiêu đề “AnimatedVisibility cho items”@Composablefun 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
Phần tiêu đề “Swipe to Dismiss”@OptIn(ExperimentalMaterial3Api::class)@Composablefun 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
Phần tiêu đề “📝 Tóm tắt”| Component | Mục đích |
|---|---|
LazyColumn |
Danh sách cuộn dọc |
LazyRow |
Danh sách cuộn ngang |
LazyVerticalGrid |
Lưới cuộn dọc |
LazyHorizontalGrid |
Lưới cuộn ngang |
LazyVerticalStaggeredGrid |
Lưới so le (Pinterest style) |
Best Practices
Phần tiêu đề “Best Practices”- Luôn sử dụng
keyvới unique, stable ID - Dùng
contentTypekhi có nhiều loại items - Tránh làm composable trong items quá nặng
- Sử dụng
derivedStateOfcho derived scroll values - Dùng Paging 3 cho large datasets