Bỏ qua để đến nội dung

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.

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

@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()
)
}
}
}
}

@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
)
}
}
}
@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)
}
}
}

@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))
}
}
}

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)
}
}
}

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)
}
}
}
}

@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)
}
}
}
}

@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")
}
}
}
@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)
}
}
}
}

@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)
)
}
}

build.gradle.kts
dependencies {
implementation("androidx.paging:paging-compose:3.3.0")
}
@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 -> {}
}
}
}

@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)
}
}
}
}
@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)
}
}

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)
  • 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