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
| 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
- 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
Last updated on