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 里同时用了 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()
                )
            }
        }
    }
}
注意: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