7、列表与网格:LazyColumn 与 LazyRow,LazyVerticalGrid,列表项点击与滑动删除
列表和网格,说白了就是 App 里最常用的内容展示方式。你刷微博、看新闻、逛商城,背后全是这些东西在撑场面。
在 Jetpack Compose 里,我们不用 RecyclerView 了,改用 LazyColumn、LazyRow 这些「懒加载」组件。为什么叫「懒」?因为它们只渲染当前屏幕能看见的 item,滑出去的就不管了——内存和性能都省了不少。
我个人习惯把 LazyColumn 当成 RecyclerView 的平替,但用起来爽太多了。你想想看,不用写 Adapter、不用管 ViewHolder,直接写 Composable 函数就行。
7.1 LazyColumn:垂直列表
LazyColumn 是最常用的。你传一个数据列表,它帮你渲染成垂直滚动的列表。
@Composable
fun SimpleList() {
val items = listOf("苹果", "香蕉", "橘子", "西瓜", "葡萄")
LazyColumn {
items(items) { fruit ->
Text(
text = fruit,
fontSize = 18.sp,
modifier = Modifier.padding(16.dp)
)
}
}
}
嗯,这里要注意:items 是 LazyListScope 里的扩展函数,不是普通的集合操作。我第一次写的时候还纳闷怎么用不了,后来才发现要写在 LazyColumn 的花括号里面。
如果你需要索引,用 itemsIndexed:
LazyColumn {
itemsIndexed(items) { index, fruit ->
Text("第${index + 1}个:$fruit")
}
}
7.2 LazyRow:水平列表
LazyRow 和 LazyColumn 几乎一模一样,只是滚动方向变成了水平。适合做轮播图、标签栏、横向分类导航。
@Composable
fun HorizontalCategoryRow() {
val categories = listOf("推荐", "热门", "最新", "科技", "生活")
LazyRow {
items(categories) { category ->
Text(
text = category,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
}
}
我在项目中遇到过一个问题:LazyRow 里的 item 宽度不一致,导致滑动时卡顿。后来发现是没给 item 设置固定宽度。建议给每个 item 一个 .width(80.dp) 之类的约束,性能会好很多。
7.3 LazyVerticalGrid:网格布局
网格布局在电商 App 里太常见了。LazyVerticalGrid 可以指定列数,或者用 Adaptive 自适应宽度。
@Composable
fun ProductGrid() {
val products = (1..20).map { "商品 $it" }
LazyVerticalGrid(
columns = GridCells.Fixed(2), // 固定2列
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(products) { product ->
Card(
modifier = Modifier.height(120.dp),
colors = CardDefaults.cardColors(containerColor = Color.LightGray)
) {
Box(contentAlignment = Alignment.Center) {
Text(product)
}
}
}
}
}
如果你想让网格自适应屏幕宽度,用 GridCells.Adaptive(minSize = 100.dp)。它会根据屏幕宽度自动计算列数。说白了就是「每个格子至少 100dp 宽,能放几个放几个」。
LazyVerticalStaggeredGrid。
7.4 列表项点击
点击事件在 Compose 里很简单,用 clickable 修饰符就行。但要注意,别把点击加在整行上然后里面还有按钮——那样会冲突。
LazyColumn {
itemsIndexed(items) { index, item ->
ListItem(
headlineContent = { Text(item) },
modifier = Modifier.clickable {
// 处理点击
println("点击了第 $index 项")
}
)
}
}
我曾经踩过一个坑:在 item 里同时用了 clickable 和 combinedClickable(长按),结果长按事件死活不触发。后来发现是 clickable 的默认延迟导致的。解决方案是显式设置 interactionSource。
7.5 滑动删除(SwipeToDismiss)
滑动删除是列表的经典交互。Compose Material3 提供了 SwipeToDismissBox(旧版叫 SwipeToDismiss)。
实现思路:每个 item 外面包一层 SwipeToDismissBox,滑动时显示删除背景,松手后执行删除逻辑。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeableList() {
val items = remember { mutableStateListOf("任务1", "任务2", "任务3", "任务4") }
LazyColumn {
items(items, key = { it }) { item ->
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { dismissValue ->
if (dismissValue == SwipeToDismissBoxValue.EndToStart) {
items.remove(item)
true
} else {
false
}
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
contentAlignment = Alignment.CenterEnd
) {
Text("删除", color = Color.White, modifier = Modifier.padding(end = 16.dp))
}
}
) {
ListItem(
headlineContent = { Text(item) },
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class) 注解。另外,confirmValueChange 里返回 true 才会执行删除,返回 false 则取消。我建议在这里做二次确认,比如弹个 Dialog,避免误删。
7.6 性能优化与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 列表滑动卡顿 | item 重组频繁 | 使用 key 参数,让 Compose 知道哪些 item 是稳定的 |
| 滑动删除动画不流畅 | 状态管理不当 | 用 remember 缓存 dismissState,不要每次重组都新建 |
| LazyColumn 嵌套 LazyRow | 双向滚动冲突 | 固定内层 LazyRow 的高度,或者用 nestedScroll 处理 |
| 列表项内容频繁变化 | 没有用 derivedStateOf |
用 remember + derivedStateOf 减少不必要的重组 |
我个人习惯在写列表时,每个 item 都加上 key。你想想看,如果不加 key,Compose 只能靠位置来识别 item,一旦数据顺序变了,整个列表都得重新渲染。加了 key 之后,它就知道「哦,这个 item 只是挪了个位置,内容没变」,性能好很多。
- LazyColumn 垂直列表、LazyRow 水平列表、LazyVerticalGrid 网格
- 点击用
clickable修饰符,注意事件冲突 - 滑动删除用
SwipeToDismissBox,记得加 Experimental 注解 - 性能优化三件套:加 key、用 remember、避免嵌套滚动
好了,这一章的内容就到这里。列表和网格是 App 的骨架,掌握了这些,大部分 UI 场景你都能应付了。