004 列表:从 RecyclerView 到 LazyColumn 的迁移心得
今天我想和大家聊聊列表,这个在移动开发里几乎无处不在的组件。如果你和我一样,是从传统的 View 体系(特别是 RecyclerView)一路走过来的,那么刚开始接触 Compose 的 LazyColumn 时,可能会下意识地寻找那些熟悉的“对应关系”。下面这张对照表,就是我最初为了理清思路而整理的,希望能帮你快速建立认知。
1. 先来一场“心智映射”
| 我们在 RecyclerView 中熟悉的 | 在 Compose 中对应的思路 |
|---|---|
Adapter + ViewHolder |
items { } / itemsIndexed + 一个 Composable lambda |
LayoutManager |
垂直列表用 LazyColumn,水平列表用 LazyRow |
DiffUtil |
为 items 提供 key = { ... },这是性能优化的关键 |
| Item 装饰或分割线 | 直接用 Divider 组件,或者在 item { } 里插入间隔 |
这里有个根本性的思维转换需要特别注意:Compose 没有 ViewHolder 那种复用同一个 View 实例的概念。它的机制是 按需组合 和 智能跳过重组。所以,我们的优化重点从“复用 View”变成了“提供稳定的 key”和“使用不可变/稳定的数据类型”,来避免整个列表不必要的重组(这个我们会在第 008 篇详细展开)。
2. 一个最基础的 LazyColumn 长什么样?
理论说再多,不如看代码。下面就是一个最简单的列表实现,我习惯从这样的“最小可行产品”开始,然后再往上添加功能。
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
data class Message(val id: String, val title: String)
@Composable
fun MessageList(messages: List<Message>, onItemClick: (Message) -> Unit) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = messages,
key = { it.id }
) { msg ->
MessageRow(msg, onClick = { onItemClick(msg) })
}
}
}
@Composable
private fun MessageRow(msg: Message, onClick: () -> Unit) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = msg.title,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyLarge
)
}
}
看,是不是很清晰?LazyColumn 块里直接使用 items 函数来遍历数据,并且我强烈建议你养成习惯,总是通过 key 参数提供一个唯一标识。这就像是给每个列表项上了户口,Compose 才能高效地判断哪些项需要更新。
3. 如何实现多种 Item 类型?(类似多 viewType)
在 RecyclerView 里我们要写 getItemViewType,在 LazyColumn 里就直观多了。你可以把 item { } 和 items { } 自由地混合编排。
LazyColumn {
item { Text("我是列表头部", modifier = Modifier.padding(8.dp)) }
items(list, key = { it.id }) { ItemRow(it) }
item {
Button(onClick = { /* 加载更多 */ }, modifier = Modifier.fillMaxWidth()) {
Text("加载更多")
}
}
}
这种声明式的方式,让列表的头部、尾部和分隔项变得非常容易管理,代码的可读性也大大提升。
4. 实现粘性头部(Sticky Header)
粘性头部是一个很常见的需求。在 Compose 中,我们可以使用 stickyHeader API,不过它目前还处于实验状态,记得加上 @OptIn 注解。
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.stickyHeader
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GroupedList(groups: List<Pair<String, List<String>>>) {
LazyColumn {
groups.forEach { (title, rows) ->
stickyHeader {
Text(
text = title,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(12.dp)
)
}
items(rows, key = { "$title-$it" }) { row ->
Text(row, modifier = Modifier.padding(16.dp))
}
}
}
}
它的使用逻辑和 item 很像,只是它会“粘”在顶部,直到被下一个粘性头部推走。构建分组列表变得异常简单。
5. 分页与“滑动到底部加载更多”
这是列表的进阶话题。常见的做法是监听 LazyListState 的状态,当用户滑动接近底部时触发加载。
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
val listState = rememberLazyListState()
LaunchedEffect(listState) {
snapshotFlow {
val info = listState.layoutInfo
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: 0
lastVisible to info.totalItemsCount
}.collect { (last, total) ->
if (total > 0 && last >= total - 3) {
// 触发 ViewModel 中的加载更多逻辑
// viewModel.loadMore()
}
}
}
LazyColumn(state = listState) { /* ... */ }
个人经验:这里的阈值(比如 total - 3)需要根据实际产品列表项的高度来调整。另外,一定要记得在 ViewModel 层做好去抖(debounce)处理,避免在快速滑动时高频触发网络请求。
当然,如果你已经在使用 Paging 3 库,那么集成会更简单,直接使用 collectAsLazyPagingItems() 即可(需要添加 paging-compose 依赖)。
6. 小心嵌套滚动!老问题,新形式
这又是一个从 RecyclerView 时代就流传下来的“祖训”:避免同向滚动的列表嵌套。在 XML 里,我们不会把 RecyclerView 放在另一个同向滚动的 RecyclerView 或 ScrollView 里。
在 Compose 中,这条原则依然有效。优先考虑使用一个 LazyColumn 来承载所有可变长度的内容。如果万不得已需要嵌套(比如一个垂直列表里嵌一个横向列表),就需要用到 LazyColumn 和 LazyRow 的嵌套滚动连接 API,这比之前要复杂一些,所以能避免就尽量避免。
7. 动手练习一下
光说不练假把式,我建议大家从这两个小练习开始:
- 迁移练习:找一个你项目中现有的
RecyclerView.Adapter,尝试把它改写成LazyColumn+items(key=...)的形式,感受一下代码量的变化和思维的转换。 - 状态处理:为你的列表加上空状态。逻辑很简单:
if (list.isEmpty()) { item { EmptyView() } } else { items(...) }。这种条件性的 UI 构建在 Compose 里写起来非常自然。
列表是应用的骨架,掌握好 LazyColumn 是迈向熟练使用 Compose 的关键一步。希望我的这些经验对你有帮助。
下一篇,我们将一起看看如何用 Compose 定义主题和颜色:005-主题Material与资源字符串.md
当前文章价值9.05元,扫一扫支付后添加微信提供帮助!(如不能解决您的问题,可以申请退款)

评论已关闭!