Android LazyRow的使用

15 阅读5分钟

一、核心原理(先理解为什么用)

  • 普通 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), // 固定2modifier = 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
                )
            }
        }
    }
}

五、性能优化(避坑关键)

  1. 复用子项 Key:默认情况下,LazyRow 按索引复用子项,若数据源发生增删 / 排序,会导致子项错误复用。解决方案:给 items 指定唯一 key
// 正确:用数据唯一ID作为key
items(dataList, key = { item -> item.id }) { item ->
    // 子项布局...
}
  1. 减少重组
  • 子项布局抽离为独立 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))
    }
  1. 控制预加载数量:通过 flingBehavior 调整预加载数量(默认预加载 1-2 个屏幕的子项),减少内存占用:
LazyRow(
flingBehavior = ScrollableDefaults.flingBehavior(
state = lazyListState,
// 预加载距离(越小,预加载越少)
snapAnimationSpec = SnapSpec(dampingRatio = 0.8f)
)
) {
    // 子项...
}
  1. 避免子项无限宽高:LazyRow 子项需指定固定宽度 / 最大宽度,否则会导致测量异常(比如 fillMaxWidth() 不适合 LazyRow 子项,改用 width()/size())。

六、常见问题 & 解决方案

问题原因解决方案
子项内容溢出子项宽度超过 LazyRow 高度 / 宽度给子项设置固定尺寸,或用 wrapContent
滚动卡顿子项布局复杂 / 未复用 Key简化子项布局、添加唯一 Key
滚动到指定位置无效未在协程中执行 scrollToItem用 rememberCoroutineScope 包裹
列表内边距无效用了 modifier.padding 而非 contentPadding改用 contentPadding 设置内边距