LazyColumn — Jetpack Compose 组件,用于显示带有惰性加载的可滚动列表。与 RecyclerView 不同,LazyColumn 不创建 ViewHolder 也不使用 XML — 所有元素都通过 composable 函数声明式地描述。我们展示什么是 LazyColumn,LazyListState 和 items API 如何工作,以及如何达到 60 FPS 的性能。在 IT Sectr,我们在所有新的 Compose 项目中使用 LazyColumn。要对比经典方法,请阅读 关于 RecyclerView 的文章。
要点
items()、itemsIndexed()、items() 函数。LazyColumn — 是来自 Foundation Compose 库的 composable 函数,用于显示带有惰性(lazy)加载的垂直元素列表:只有屏幕上可见或附近的元素(预取区域)才会被创建和组合。LazyColumn 在 Compose 1.0(2021 年)中与 LazyRow(水平列表)和 LazyVerticalGrid(网格)一起出现。
惰性加载通过 SubcomposeLayout 机制工作:LazyColumn 测量可用空间,从 LayoutInfo 请求可见范围,并仅组合该范围内的元素。离开屏幕的元素离开组合(预取缓冲区除外)。根据 Google 的数据(Android Performance,2026),LazyColumn 在中端设备上滚动 10000+ 个元素的列表时可提供 60 FPS。
Column 带有垂直滚动 — 是应用 verticalScroll 修饰符将所有元素彼此堆叠的常规排列。Column 不是惰性的:所有子元素立即被组合,即使它们不可见。对于 200+ 个元素的列表,Column 在启动时会引起显著的延迟。LazyColumn 只组合可见元素 + 缓冲区(默认 1 个屏幕向前/向后),使其成为长列表不可或缺的组件。
items API — LazyColumn 的一组扩展函数,接受数据列表和每个元素的 DSL 描述符。主要函数:items(count, key, itemContent) 用于固定数量,items(list, key, itemContent) 用于列表,itemsIndexed(list, key, itemContent) 带索引。key 参数 — 性能的必要条件 — 为 Compose 提供稳定的元素标识符。
// 带有 itemContent 的基本 LazyColumn
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Header — 不是惰性的,总是在开头
item {
Text("最新文章", style = MaterialTheme.typography.headlineMedium)
}
// 以 id 为键的文章列表
items(articles, key = { it.id }) { article ->
ArticleCard(
title = article.title,
summary = article.summary,
onClick = { onArticleClick(article.id) }
)
}
// 带有加载指示器的 Footer
item {
CircularProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(16.dp)
)
}
}
}
// 自定义列表元素
@Composable
fun ArticleCard(title: String, summary: String, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(4.dp))
Text(text = summary, style = MaterialTheme.typography.bodyMedium)
}
}
}
item {} vs items():对于列表中始终存在的单个元素(header、footer、分隔符),使用 item {}。对于动态数据列表,使用 items()。items() 接受 Iterable 或 Int 以及为每个元素调用的 DSL 块。在同一个 LazyColumn 中组合 item 和 items:调用顺序决定了显示顺序。
LazyListState — 存储列表状态的对象:当前滚动位置(firstVisibleItemIndex、firstVisibleItemScrollOffset)、可见元素(layoutInfo)和程序化滚动方法(scrollToItem、animateScrollToItem)。LazyListState 通过 rememberLazyListState() 创建,并通过 state 参数传递给 LazyColumn。
// 保留滚动位置的 LazyColumn
@Composable
fun ScrollingList(items: List<Item>) {
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(state = listState) {
items(items, key = { it.id }) { item ->
ListItemView(item = item)
}
}
// “向上”按钮在第 10 个元素后出现
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 10 }
}
AnimatedVisibility(visible = showButton) {
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
onClick = {
coroutineScope.launch {
listState.animateScrollToItem(0)
}
}
) {
Icon(Icons.Default.KeyboardArrowUp, "回到顶部")
}
}
}
}
// 获取可见元素的信息
@Composable
fun ListDebugInfo(listState: LazyListState) {
val visibleItems = listState.layoutInfo.visibleItemsInfo
val totalItems = listState.layoutInfo.totalItemsCount
val firstItem = visibleItems.firstOrNull()?.index
val lastItem = visibleItems.lastOrNull()?.index
Text("显示 $firstItem–$lastItem,共 $totalItems")
}
程序化滚动通过 listState 的 suspend 函数执行:scrollToItem(index, scrollOffset) — 即时移动(无动画),animateScrollToItem(index) — 带动画。要从协程滚动,请使用 LaunchedEffect 或 coroutineScope.launch。重要提示:不要从组合内部(@Composable 内)调用 suspend 函数 — 使用 lambda(onClick、LaunchedEffect)。
LazyColumn 和 RecyclerView 解决相同的任务 — 高效显示长列表。区别在于架构:RecyclerView 使用 ViewHolder 和 Adapter(经典 Android View),LazyColumn — 使用没有 XML 和 ViewHolder 的声明式 composable 函数。选择取决于项目的技术栈:Compose vs View-based UI。
| 参数 | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| 布局 | Composable 函数 (Kotlin DSL) | XML 文件 + ViewBinding |
| 适配器 | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| 排序/过滤 | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| 动画 | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| 不同元素类型 | item {}/items() + 按类型的 when | getItemViewType + 多个 ViewHolder |
| 滚动到位置 | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| 预取 | 进程内 (SubcomposeLayout 缓冲区) | RecycledViewPool + GapWorker |
| 网络/分页 | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
Google 建议(Android Developers,2026):对于 Jetpack Compose 上的新项目,使用 LazyColumn。对于 View-based 项目,使用 RecyclerView。LazyColumn 需要 Compose,这会给 APK 增加大约 3–5 MB — 在支持旧设备时请考虑这一点。如果项目已经在使用 View 系统,LazyColumn 在没有 AndroidView 的情况下不能与 XML 布局共存于同一屏幕 — 在这种情况下,更简单的做法是继续使用 RecyclerView。
LazyVerticalGrid — 用于具有固定列数(columns)的网格的 LazyColumn 版本。LazyHorizontalGrid — 具有固定行数(rows)的水平网格。两个组件使用与 LazyColumn 相同的惰性加载原则和相同的 items API。columns 参数通过 GridCells.Fixed(N) 或 GridCells.Adaptive(minSize) 确定列数。
// 自适应列的网格
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(photos, key = { it.id }) { photo ->
AsyncImage(
model = photo.url,
contentDescription = photo.title,
modifier = Modifier.aspectRatio(1f)
)
}
}
}
// 水平列表 (LazyRow)
@Composable
fun CategoryCarousel(categories: List<Category>) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(categories, key = { it.id }) { category ->
FilterChip(
selected = category.isSelected,
onClick = { onCategoryClick(category.id) },
label = { Text(category.name) }
)
}
}
}
GridCells.Adaptive 根据单元格的最小大小自动计算列数。例如,GridCells.Adaptive(128.dp) 在 360 dp 宽的屏幕上放置 2 列,在 600 dp 的平板电脑上放置 4 列。GridCells.Fixed(2) 始终显示正好 2 列。Adaptive 更适合多屏幕适配 — 它无需媒体查询即可自动适应屏幕宽度。
性能 LazyColumn 取决于三个因素:键的稳定性(key)、内容类型(contentType)和预取缓冲区(beyondBounds)。默认情况下,LazyColumn 缓冲 1 个屏幕向前和 1 个向后。Google(Android Developers,2026)建议为具有不同元素类型或复杂组合的列表配置这些参数。
| 参数 | 描述 | 建议 |
|---|---|---|
| key | 元素的稳定标识符 | 始终指定 key = { it.id }。没有 key,Compose 使用索引,元素的重新排序会破坏动画。 |
| contentType | 用于分隔池的内容类型 | 为具有不同元素类型(文本 + 图片 + 广告)的列表指定。Compose 只会在相同的 contentType 内重用组合。 |
| beyondBounds | 预取的屏幕数 | 对于包含大图的列表,增加到 2–3。默认为 1 个屏幕。 |
| Modifier.animateItem | 位置变化动画 | 用于动画排序和过滤(Compose 1.7+)。 |
// 使用 contentType 和 prefetch 优化的 LazyColumn
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 2 // 预取 2 个屏幕
) {
items(
items = items,
key = { it.id },
contentType = { it.type } // 按类型分隔
) { item ->
when (item) {
is FeedItem.Post -> PostView(item)
is FeedItem.Ad -> AdView(item)
is FeedItem.Suggested -> SuggestedView(item)
}
}
}
}
// 通过 listState 测量性能
@Composable
fun ScrollPerformance(listState: LazyListState) {
val scrollInfo = listState.layoutInfo
val visibleCount = scrollInfo.visibleItemsInfo.size
val total = scrollInfo.totalItemsCount
// derivedStateOf — 如果值未更改则不重新组合
val scrollProgress by remember {
derivedStateOf {
if (total > 0) visibleCount.toFloat() / total else 0f
}
}
LinearProgressIndicator(
progress = scrollProgress,
modifier = Modifier.fillMaxWidth().height(2.dp)
)
}
常见错误:(1)缺少 key — 元素在每次列表更改时重新组合;(2)使用没有 snapshot 流的 mutableStateListOf — 更改可能不会被跟踪;(3)在 itemContent 内调用 suspend 函数 — 中断组合;(4)混合类型没有 contentType — Compose 重用广告的组合用于文本帖子,导致视觉伪影。在 IT Sectr,我们为所有包含三种及以上内容类型的列表添加 contentType。
常见问题
检查三个参数:(1)key — 没有稳定的键,Compose 无法重用组合;(2)contentType — 用于具有不同元素类型的列表;(3)beyondBoundsPageCount — 对于图片增加到 2。将 itemContent 内的重计算移到带有数据键的 remember 中。对于简单的图形,使用 Modifier.drawWithContent 代替 Image。
使用 LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) 设置元素间距。对于带有线条的视觉分隔符,在 items 之间添加 item { Divider() }。自动分隔符:items(items, key = { it.id }) { item -> ... },并在每个元素之间插入 item { HorizontalDivider() }。为此使用 LazyListScope.items(list, key) { /* 元素 */ } 和独立的 LazyListScope.item { Divider() }。
LazyColumn 默认具有无限高度(fillMaxSize)。在没有固定高度的 Column 内部,LazyColumn 无法确定尺寸。解决方案:(1)为 LazyColumn 设置固定高度:Modifier.height(400.dp);(2)在 Column 内部使用 Modifier.weight(1f);(3)不要将 LazyColumn 放在垂直滚动容器中 — 使用 LazyColumn 作为根元素,用 item {} 用于 header 和 footer。
LazyColumn 自动响应 State 的变化。如果数据存储在 mutableStateListOf 或 mutableStateOf 中,Compose 仅重新组合已更改的元素。对于来自 ViewModel 的列表,使用带有 Flow 的 collectAsState()。当使用 key 更新列表时,Compose 会通过 Modifier.animateItemPlacement() 自动动画化更改(添加、删除、移动)。
LazyColumn — 用于单列列表(信息流、聊天、评论)。LazyVerticalGrid — 用于网格(相册、目录、图标)。如果元素在手机上应为一列,在平板电脑上为两列 — 使用带有 GridCells.Adaptive 的 LazyVerticalGrid。如果需要混合网格(一个部分中不同数量的列)— 使用 LazyColumn,并在 items 内部插入水平 LazyRow 或嵌套的 LazyVerticalGrid。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。