拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Jetpack Compose 列表开发:从基础到性能优化全解析

Jetpack Compose 的声明式 UI 开发做到列表这一课算是真正进入了日常 Android 开发的高频区域。列表在安卓应用里太常见了通讯录、订单、推荐流、消息记录几乎每个项目都离不开它。Compose 里的列表不是把 RecyclerView 换个名字而是把“数据是什么、列表长什么样、用户怎么交互”三个问题放在一起解决。这篇文章适合刚开始用 Compose 写列表、或者准备在简历项目里加入 Compose 的同学。我会按实际开发顺序把列表环境、基础写法、参数含义、状态处理、性能优化和常见问题拆开讲。最值得先看的是 LazyColumn 的性能边界能跑通很简单跑得稳才是关键。1. 先理解声明式列表再动手写 LazyColumn1.1 声明式 UI 到底在说什么过去用 RecyclerView 写列表流程非常固定先准备数据再写 Adapter 和 ViewHolder最后通过 notifyDataSetChanged 或 DiffUtil 通知界面刷新。数据一变你就得想“哪些条目需要更新、动画怎么触发”。这套模式不是不能写而是当业务逻辑复杂以后界面和数据的同步变成了负担。Compose 换了一个思路。它把你当前持有的数据看成“状态”。你只需要声明“当前状态映射成什么界面”剩下的刷新工作交给系统。数据变化后Compose 会重新执行对应的 Composable找出真正变化的部分再更新界面。这个过程叫重组。对列表来说差异非常明显。RecyclerView 里Adapter 的 getItemCount、onCreateViewHolder、onBindViewHolder 是三个必写的方法。Compose 里你只需要写“这个列表有多少条、每条长什么样”。代码量不一定减少很多但思路从“流程驱动”变成了“状态驱动”。这一点在列表项内部有按钮、输入框、图片时体会最深。1.2 为什么列表要用 LazyColumn而不是 Column这个问题几乎每个新手都会遇到。如果你只有三五条数据用 Column 加 for 循环也能显示。但数据稍微多一点比如几百上千条Column 会一次性把所有 item 全部组合出来。也就是说屏幕外的 item 也会参与测量和绘制内存和首帧时间都会明显上涨。LazyColumn 解决的问题就在这里它只组合当前可见区域附近的 item滚动时按需创建和销毁。Lazy 这个单词不是随便叫的它和 RecyclerView 的“回收复用”在目标上一致但实现方式不同。RecyclerView 依赖 View 的复用机制LazyColumn 依赖 Compose 的组合范围管理。你不需要深入阅读源码但要知道一件事长列表、数量不确定的列表、需要滚动加载的列表直接用 LazyColumn固定几个卡片、不需要滚动的短列表再用 Column。1.3 容器类型先选对后面才少返工Compose 里和列表相关的容器不是只有 LazyColumn 一种。垂直列表LazyColumn对应 RecyclerView 的 LinearLayoutManager。横向列表LazyRow对应横向滑动列表。网格LazyVerticalGrid对应 GridLayoutManager。瀑布流LazyVerticalStaggeredGrid需要额外依赖。标题里说的“列表”最常见的场景就是 LazyColumn。我建议先把 LazyColumn 吃透再去看 LazyRow 和网格。这几个组件的参数、状态管理、性能边界基本一致后面无非是换一个滚动方向和布局维度。不要一上来就把所有列表组件都写进项目很容易因为参数混用导致理解混乱。注意第一次做列表先跑通 LazyColumn。能跑通之后再考虑横向、网格和嵌套滚动。2. 环境、依赖和第一个可运行列表2.1 最低环境能跑什么Compose 对 Android Studio 和 Gradle 版本有要求但不需要把版本号背下来。用较新的稳定版 Android Studio 新建项目时选择 Empty Activity 并且语言是 Kotlin模板里通常已经带了 Compose 支持。如果你是在已有项目里接入 Compose需要先确认三件事Gradle 插件版本是否兼容。Kotlin 版本和 Compose 编译器插件是否匹配。模块是否开启了 buildFeatures { compose true }。新建项目一般没问题老项目迁移时最容易踩版本坑。版本不匹配时编译阶段会报错提示通常比较直接照着版本号改即可。Compose 编译器插件在较新版本里会跟着 Kotlin 一起配置不同时代的写法不一样遇到编译失败先检查这一项不要急着改代码。2.2 依赖怎么加Compose 的依赖按功能模块拆分。写列表最少需要这样// build.gradle.kts (Module) android { buildFeatures { compose true } } dependencies { implementation(platform(androidx.compose:compose-bom:你的BOM版本)) implementation(androidx.compose.ui:ui) implementation(androidx.compose.foundation:foundation) implementation(androidx.compose.material3:material3) }LazyColumn 在 foundation 包里Material3 用来提供主题和基础组件。如果你的项目已经用 Material2也可以暂时不引入 Material3但新项目建议直接用 Material3。版本号这里我不写死因为 Compose 版本更新太快写了也容易过时。你把依赖写进 Gradle 后Android Studio 通常会提示可用的稳定版本或者直接参考官方模板里的版本。别从网上随手复制一个 BOM 版本就完事要和你项目当前的 Kotlin 版本匹配。2.3 跑通最小列表先写一个最简单的列表数据来自本地内存不涉及网络。Composable fun SimpleList() { val data List(50) { 第 ${it 1} 条数据 } LazyColumn { items(data) { text - Text( text text, modifier Modifier .fillMaxWidth() .padding(16.dp) ) } } }这段代码能显示 50 条文字并且支持滚动。跑通之后你会看到 LazyColumn 的基本形态items 函数接收一个 List并为每条数据调用后面的 lambdalambda 里写 Item 的界面。如果点击运行后是空白先确认这个 Composable 有没有被 setContent 调用再确认 LazyColumn 有没有被放进一个有宽高的界面里。这两点是新手最常见的问题。2.4 数据源怎么组织示例里的 List 直接写死没问题但真实项目里列表数据一般来自 ViewModel 的 StateFlow 或 LiveData。写列表前先把数据模型定义清楚。data class Contact( val id: Long, val name: String, val avatarUrl: String, val phone: String )Compose 里列表数据通常用不可变 List 包装。更新列表时不是往原 List 里 add而是产生一个新的 List。这一点和 RecyclerView 的“修改集合后 notify”完全不同。如果写了 add 但界面不动大概率是状态没有用 State 包装或者 LiveData/Flow 没有在界面里正确 collect。列表能显示之后数据源换成 ViewModel你会发现界面刷新逻辑反而比 RecyclerView 简单很多。3. LazyColumn 参数这么多哪些值得优先掌握3.1 items 系列函数怎么选LazyColumn 的常见写法有两种。items(items dataList) { item - ListItem(item) }itemsIndexed(items dataList) { index, item - // 需要 index 时使用 }如果你的数据源是 List用 items如果需要知道下标用 itemsIndexed如果数据源是一个数字区间可以用 items(count n)。不要为了拿一个下标单独给数据包装一个 index 字段除非业务数据本身就包含排序号。还有一种情况需要注意列表里可能出现多种 item 类型比如消息列表里既有文本消息又有图片消息。这时可以给 items 传 contentType 参数提示 LazyColumn 按类型划分组合单元减少滚动时不必要的重组。数据种类多、item 布局差异大的时候这个参数很有价值。3.2 间距、边距和分割线列表项之间需要间距时最简单的是在每个 item 的 Modifier 上加 padding。但这样首尾也会留出空间布局不精确时会出现上下不对称。更推荐用 Arrangement.spacedByLazyColumn( verticalArrangement Arrangement.spacedBy(8.dp) ) { ... }这样会在相邻 item 之间插入 8dp 间距第一项和最后一项不会额外加间距。如果还想要列表整体距离屏幕边缘有边距用 contentPaddingLazyColumn( contentPadding PaddingValues(horizontal 16.dp, vertical 12.dp), verticalArrangement Arrangement.spacedBy(8.dp) ) { ... }contentPadding 作用于滚动内容的裁剪范围滚动到最后一项时内容不会贴到底部。这个细节对“滚动到底部时 UI 是否被遮挡”影响很大。需要分割线时可以在 item 之间插入一个 HorizontalDivider也可以直接在 item 布局底部画分割线。我更建议把分割线做成单独 item位置更可控。3.3 key 参数不是可有可无RecyclerView 里 item 需要稳定的 idLazyColumn 同样支持 key。给 items 传 key 后Compose 在数据变化时能更准确地定位 item 的位置和状态。items(items contacts, key { it.id }) { contact - ContactItem(contact) }最好给每条数据绑定一个唯一 id比如数据库主键。没有 key 时如果列表中间插入或删除数据Compose 可能会把后面的 item 当作复用对象导致某些状态错乱比如输入框内容跑到另一行。有 key 后Compose 能识别“这条数据还是原来那条”只是位置变了。列表项里有输入状态、选中状态、动画状态时这个参数尤其重要。3.4 列表项要抽成独立组件Compose 的 item 就是一个 Composable可以写任意组件。复杂 item 建议抽成独立函数方便复用和预览。Composable fun ContactItem( contact: Contact, onClick: () - Unit ) { Row( modifier Modifier .fillMaxWidth() .clickable(onClick onClick) .padding(12.dp), verticalAlignment Alignment.CenterVertically ) { // 头像、姓名、电话 } }抽出来的好处很直接预览时可以单独看一个 item数据字段变化时只改一个地方列表数量变大后性能分析也能定位到具体 item。很多初学者把整个列表逻辑塞在一个 Composable 里最后不只是代码难看排查问题也要翻很久。4. 列表项的点击、状态和加载更多4.1 点击事件不要写进数据类列表项点击一般有两种做法在 item 内部写 onClick或在 item 外面把点击回调传给 item。推荐第二种。数据类不应该持有界面行为item 的职责是展示数据、触发回调具体逻辑由调用方决定。LazyColumn { items(contacts, key { it.id }) { contact - ContactItem( contact contact, onClick { openDetail(contact.id) } ) } }如果你在 item 里直接写了 navigation 或弹窗后面想复用这个 item 时就得把逻辑拆出来。回调式设计在 Compose 里算是默认惯例写起来顺手测试也好写。4.2 记住列表滚动位置页面旋转、导航返回后列表位置丢失是常见问题。App 进程还存活时最简单的做法是用 rememberSaveable 配合 LazyListState或者把 LazyListState 保存在 ViewModel 里。val listState rememberLazyListState() LazyColumn(state listState) { ... }需要精确恢复位置时可以在 ViewModel 里保存 firstVisibleItemIndex 和 firstVisibleItemScrollOffset。rememberSaveable 适合轻量状态如果列表本身很大自己保存下标和偏移量更可靠。判断标准很简单返回页面时如果希望用户停留在他离开的位置就必须保存列表状态如果只是一个临时列表回到页面重置也没关系就不用加。4.3 加载更多怎么实现更稳加载更多是列表开发里绕不开的需求。核心思路是在 LazyColumn 中追加一个 item显示“加载中”或“加载失败”当这个 item 进入可见区域时触发加载。val listState rememberLazyListState() val lastVisibleItemIndex by remember { derivedStateOf { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 } } LaunchedEffect(lastVisibleItemIndex) { if (lastVisibleItemIndex listState.layoutInfo.totalItemsCount - 1) { viewModel.loadNextPage() } }这里的关键是不要监听滚动事件手动判断而是用 derivedStateOf 把“最后一个可见 item 的 index”变成状态。index 接近底部时自动加载。同时需要控制加载锁防止一次触发多个请求。凡是接口请求至少要有三种状态加载中、成功、失败。失败后提供一个“点击重试”入口比静默失败好得多。4.4 下拉刷新和空状态Material3 里下拉刷新需要单独依赖用户下拉时触发刷新回调。空列表时要显示空状态视图告诉用户“当前没有数据”。很多新手只写了数据列表接口返回空数组时页面一片白体验很差。列表页开发应该把四种状态都测一遍正常数据、空数据、加载失败、加载中。这四种状态对应不同的界面代码结构在设计时就要留出位置。5. 性能、资源占用和批量数据经验5.1 先看 item 复杂度再谈性能优化列表卡顿第一反应不要是换组件、加 key、改 remember。先问三个问题数据量有多少item 里有多少图片每次重组时有没有做耗时操作很多情况不是 LazyColumn 的问题而是 item 里图片处理、字符串拼接、状态读取出了问题。一个 item 如果包含大量图片和高成本布局一屏只能显示几个LazyColumn 再优化也救不了。先把 item 拆小减少嵌套层级通常比调参数更有效。列表项里的大段文本、复杂阴影、多个动画都会影响滚动帧率。5.2 图片加载必须用异步库Compose 里直接同步加载图片一定会卡。实际项目建议使用 Coil 或 Glide 的 Compose 扩展。图片列表最需要注意的不是加载库选型而是不要在小尺寸组件上加载原图不要在滚动时发起大量请求缓存策略要按应用场景配置。判断标准滚动时图片闪烁、出现旧图、内存上涨通常是图片加载库没有正确设置占位图、缩略图和缓存 key。先处理这几个问题再考虑是不是 LazyColumn 的问题。图片列表的 item 大小如果是 100dp就不要加载 2000px 的原图。使用图片库的请求尺寸参数内存会明显下降。5.3 减少不必要的重组Compose 性能优化的核心是减少重组范围。列表项里如果有一个会频繁变化的文本比如进度百分比整个 item 可能会跟着频繁重组。这时可以把变化部分抽到单独的子 Composable并用 remember 或 derivedStateOf 控制更新范围。lambda 如果每次创建新对象也会增加一点重组成本但这些不用一开始就过度优化。先跑通功能再打开 Layout Inspector 看重组次数。如果一个 item 滚动时每秒重组几十次就要注意了。如果只是偶尔一次两次可以接受。5.4 批量数据分页、并发和失败重试热词里有人搜“列表切片”还有人问“vue 列表怎么做到 C 端效果”在 Compose 里同样适用一个原则批量展示数据时要把“加载、展示、更新、失败重试”四步分开处理。数据量 1 万条时LazyColumn 可以显示但如果一次性发起 1 万个网络请求或者把所有数据都塞进内存做排序过滤性能一定崩。批量任务的正确做法是分页拉取、按需展示、加载状态可见、失败任务可重试。这和列表是不是 Compose 无关是数据层的设计问题。注意低配机器能跑通小列表不代表能扛住大数据量。批量展开前先拿 1000 条数据和 10 万条数据分别测一下启动时间和滚动流畅度。6. 列表常见报错按这个顺序排查6.1 列表空白什么日志都没有先确认集合是不是空的。再确认 items 传的数据源和界面当前读取的变量是不是同一个。Compose 里常见错误是界面读取的数据来自一个状态而你要展示的列表数据来自另一个状态。改完数据后界面没刷新基本都是状态没有建立关联。调试方法在列表里加一条 Text 显示数据数量。如果数量为 0是数据层问题如果数量正常但页面空白是组合或布局问题。这样能快速缩小范围。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门