一、核心原理(先理解为什么用)
- 普通 Row:会一次性渲染所有子组件,即使子项超出屏幕范围,性能差(比如 100 个横向标签,Row 会全部渲染);
- LazyRow:采用 “懒加载” 机制,仅渲染当前屏幕可见 + 少量 “预加载” 的子项,滑动时回收不可见子项,大幅降低内存占用和绘制耗时,适合长列表 / 动态列表场景。
二、基础用法(入门必会)
1. 最简示例(固定列表)
@Composable
fun BasicLazyRowDemo() {
// 模拟数据源
val dataList = listOf("标签1", "标签2", "标签3", "标签4", "标签5", "标签6", "标签7", "标签8")
// 基础 LazyRow:横向滚动列表
LazyRow(
modifier = Modifier
.fillMaxWidth() // 宽度占满
.height(60.dp), // 固定高度(LazyRow 需指定高度,否则默认 wrapContent)
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) // 列表内边距(避免子项贴边)
) {
// 遍历数据源,创建子项(items 是 LazyRow 的核心 DSL)
items(dataList) { itemText ->
// 每个子项的布局(示例:圆角标签)
Text(
text = itemText,
modifier = Modifier
.padding(end = 8.dp) // 子项间距
.background(Color.LightGray, RoundedCornerShape(20.dp))
.padding(horizontal = 16.dp, vertical = 8.dp),
textAlign = TextAlign.Center
)
}
}
}
效果:横向滚动的标签列表,仅渲染可见的标签,滑动时动态加载 / 回收子项。
2. 核心 DSL 说明
LazyRow 的内容通过LazyListScope DSL 构建,核心函数:
| 函数 | 用途 | 示例 |
|---|---|---|
items(list) | 遍历集合创建子项(最常用) | items(dataList) { item -> ... } |
items(count) | 按数量创建子项(无数据源场景) | items(100) { index -> ... } |
item() | 添加单个子项(列表头部 / 尾部) | item { Text("头部") } |
示例:混合单个子项 + 列表项
LazyRow {
// 单个头部项
item {
Text(
text = "推荐标签",
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
fontWeight = FontWeight.Bold
)
}
// 列表项
items(dataList) { itemText ->
// 子项布局...
}
}
三、关键属性详解(核心配置)
LazyRow 的参数决定了列表的滚动行为、对齐方式、间距等,核心参数如下:
LazyRow(
modifier: Modifier = Modifier, // 布局修饰符(宽高、背景等)
state: LazyListState = rememberLazyListState(), // 列表状态(滚动位置、是否滑动等)
contentPadding: PaddingValues = PaddingValues(0.dp), // 列表内边距(避免子项贴边)
reverseLayout: Boolean = false, // 反转布局(子项从右往左排列)
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, // 子项水平排列方式
verticalAlignment: Alignment.Vertical = Alignment.Top, // 子项垂直对齐方式
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), // 滑动惯性行为
userScrollEnabled: Boolean = true, // 是否允许用户滑动
content: LazyListScope.() -> Unit // 子项内容
)
1. 常用属性实战
(1)控制子项间距 & 对齐
LazyRow(
modifier = Modifier.fillMaxWidth().height(80.dp),
contentPadding = PaddingValues(horizontal = 16.dp), // 列表左右内边距
horizontalArrangement = Arrangement.spacedBy(12.dp), // 子项之间的间距(替代手动 padding)
verticalAlignment = Alignment.CenterVertically // 子项垂直居中
) {
items(dataList) { itemText ->
Text(
text = itemText,
modifier = Modifier
.background(Color.Blue.copy(alpha = 0.1f), RoundedCornerShape(8.dp))
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
(2)列表状态(LazyListState)
用于控制 / 监听列表滚动(比如滚动到指定位置、获取当前滚动偏移),需用 rememberLazyListState()缓存状态:
@Composable
fun LazyRowWithState() {
val dataList = listOf("标签1", "标签2", "标签3","标签4","标签5","标签6","标签7", "标签8", "标签9", "标签100")
// 记住列表状态(避免重组时重置)
val lazyListState = rememberLazyListState()
// 协程作用域(用于滚动操作)
val coroutineScope = rememberCoroutineScope()
Column {
// 控制按钮:滚动到第5个位置
Button(
onClick = {
coroutineScope.launch {
// 滚动到指定索引的子项(animateScrollToItem 带动画,scrollToItem 无动画)
lazyListState.animateScrollToItem(index = 5)
}
},
modifier = Modifier.padding(16.dp)
) {
Text("滚动到第6个标签")
}
// LazyRow 绑定状态
LazyRow(
state = lazyListState,
modifier = Modifier.fillMaxWidth().height(60.dp)
) {
items(dataList) { itemText ->
Text(
text = itemText,
modifier = Modifier.padding(end = 8.dp).background(Color.LightGray).padding(8.dp)
)
}
}
}
}
(3)反转布局(reverseLayout)
设置 reverseLayout = true,子项从右往左排列(适合 RTL 语言 / 反向滚动场景):
LazyRow(
reverseLayout = true, // 子项从右到左排列
modifier = Modifier.fillMaxWidth().height(60.dp)
) {
items(dataList) { itemText ->
Text(text = itemText, modifier = Modifier.padding(start = 8.dp)) // 注意间距改为 start
}
}
四、高级使用场景
场景 1:带点击事件的子项(如横向商品列表)
@Composable
fun LazyRowWithClick() {
// 模拟商品数据
data class Product(val id: Int, val name: String, val price: String)
val productList = listOf(
Product(1, "手机", "¥2999"),
Product(2, "耳机", "¥199"),
Product(3, "平板", "¥1999"),
Product(4, "手表", "¥899")
)
LazyRow(
modifier = Modifier.fillMaxWidth().height(200.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(productList) { product ->
// 商品卡片(可点击)
Column(
modifier = Modifier
.width(120.dp)
.clickable {
// 点击事件:跳转到商品详情
println("点击商品:${product.name}")
}
.background(Color.White, RoundedCornerShape(8.dp))
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 占位图片(实际项目用 Coil/Glide 加载)
Box(
modifier = Modifier
.size(80.dp)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text("图片")
}
Text(text = product.name, fontSize = 14.sp, modifier = Modifier.padding(top = 8.dp))
Text(text = product.price, fontSize = 12.sp, color = Color.Red, modifier = Modifier.padding(top = 4.dp))
}
}
}
}
场景 2:网格横向列表(LazyGrid + 横向)
如果需要横向的网格列表(比如 2 行 N 列),用 LazyHorizontalGrid(替代 LazyRow + 嵌套 Column):
@Composable
fun LazyHorizontalGridDemo() {
val dataList = (1..20).map { "Item $it" }
LazyHorizontalGrid(
rows = GridCells.Fixed(2), // 固定2行
modifier = Modifier.fillMaxWidth().height(120.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(dataList) { item ->
Text(
text = item,
modifier = Modifier
.background(Color.LightGray, RoundedCornerShape(4.dp))
.padding(8.dp)
.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
场景 3:无限滚动列表(加载更多)
结合 LazyListState 监听滚动到底部,实现 “加载更多”:
@Composable
fun LazyRowLoadMore() {
var dataList by remember { mutableStateOf((1..20).map { "Item $it" }) }
val lazyListState = rememberLazyListState()
var isLoading by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
// 监听滚动:当滚动到倒数第3项时,加载更多
LaunchedEffect(lazyListState) {
snapshotFlow {
// 获取最后一个可见项的索引
lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
}.collect { lastVisibleIndex ->
val totalItems = dataList.size
// 滚动到倒数第3项,且未加载中
if (lastVisibleIndex >= totalItems - 3 && !isLoading) {
isLoading = true
// 模拟网络请求(延迟1秒)
coroutineScope.launch {
delay(1000)
// 追加数据
dataList = dataList + (totalItems + 1..totalItems + 10).map { "Item $it" }
isLoading = false
}
}
}
}
LazyRow(
state = lazyListState,
modifier = Modifier.fillMaxWidth().height(60.dp),
contentPadding = PaddingValues(16.dp)
) {
items(dataList) { item ->
Text(
text = item,
modifier = Modifier
.padding(end = 8.dp)
.background(Color.LightGray)
.padding(8.dp)
)
}
// 加载中占位项
if (isLoading) {
item {
CircularProgressIndicator(
modifier = Modifier.padding(start = 8.dp),
strokeWidth = 2.dp,
size = 20.dp
)
}
}
}
}
五、性能优化(避坑关键)
- 复用子项 Key:默认情况下,LazyRow 按索引复用子项,若数据源发生增删 / 排序,会导致子项错误复用。解决方案:给
items指定唯一key:
// 正确:用数据唯一ID作为key
items(dataList, key = { item -> item.id }) { item ->
// 子项布局...
}
- 减少重组:
- 子项布局抽离为独立 Composable 函数,并使用
remember缓存不变数据; - 避免在
items内部创建新对象(如Modifier/Color),否则会触发频繁重组:
// 错误:每次重组都会创建新的 Shape
items(dataList) {
Text(modifier = Modifier.background(Color.Gray, RoundedCornerShape(8.dp)))
}
// 正确:缓存 Shape
val shape = remember { RoundedCornerShape(8.dp) }
items(dataList) {
Text(modifier = Modifier.background(Color.Gray, shape))
}
- 控制预加载数量:通过
flingBehavior调整预加载数量(默认预加载 1-2 个屏幕的子项),减少内存占用:
LazyRow(
flingBehavior = ScrollableDefaults.flingBehavior(
state = lazyListState,
// 预加载距离(越小,预加载越少)
snapAnimationSpec = SnapSpec(dampingRatio = 0.8f)
)
) {
// 子项...
}
- 避免子项无限宽高:LazyRow 子项需指定固定宽度 / 最大宽度,否则会导致测量异常(比如
fillMaxWidth()不适合 LazyRow 子项,改用width()/size())。
六、常见问题 & 解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 子项内容溢出 | 子项宽度超过 LazyRow 高度 / 宽度 | 给子项设置固定尺寸,或用 wrapContent |
| 滚动卡顿 | 子项布局复杂 / 未复用 Key | 简化子项布局、添加唯一 Key |
| 滚动到指定位置无效 | 未在协程中执行 scrollToItem | 用 rememberCoroutineScope 包裹 |
| 列表内边距无效 | 用了 modifier.padding 而非 contentPadding | 改用 contentPadding 设置内边距 |