7、列表与网格:LazyColumn 与 LazyRow,LazyVerticalGrid,列表项点击与滑动删除

列表和网格,说白了就是 App 里最常用的内容展示方式。你刷微博、看新闻、逛商城,背后全是这些东西在撑场面。

在 Jetpack Compose 里,我们不用 RecyclerView 了,改用 LazyColumn、LazyRow 这些「懒加载」组件。为什么叫「懒」?因为它们只渲染当前屏幕能看见的 item,滑出去的就不管了——内存和性能都省了不少。

我个人习惯把 LazyColumn 当成 RecyclerView 的平替,但用起来爽太多了。你想想看,不用写 Adapter、不用管 ViewHolder,直接写 Composable 函数就行。

Lazy 组件家族 LazyColumn 垂直滚动列表 LazyRow 水平滚动列表 LazyVerticalGrid 垂直网格布局 核心特性 • 懒加载:只渲染可见项 • 支持 items、itemsIndexed 等 DSL 写法 • 内置 item 点击、滑动删除支持 • 可配合 animateItemPlacement 做动画

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 宽,能放几个放几个」。

小技巧:LazyVerticalGrid 和 LazyVerticalStaggeredGrid(瀑布流)是两回事。瀑布流每个 item 高度可以不同,适合图片墙。如果你需要瀑布流,用 LazyVerticalStaggeredGrid

7.4 列表项点击

点击事件在 Compose 里很简单,用 clickable 修饰符就行。但要注意,别把点击加在整行上然后里面还有按钮——那样会冲突。

LazyColumn {
    itemsIndexed(items) { index, item ->
        ListItem(
            headlineContent = { Text(item) },
            modifier = Modifier.clickable {
                // 处理点击
                println("点击了第 $index 项")
            }
        )
    }
}

我曾经踩过一个坑:在 item 里同时用了 clickablecombinedClickable(长按),结果长按事件死活不触发。后来发现是 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()
                )
            }
        }
    }
}
注意:SwipeToDismissBox 目前还是 Experimental API,需要加 @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 场景你都能应付了。


公众号:蓝海资料掘金营,微信deep3321