尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Jetpack Compose列表开发全攻略:LazyColumn实战与性能优化

Jetpack Compose列表开发全攻略:LazyColumn实战与性能优化 这次我们来看 Jetpack Compose 做安卓声明式 UI 开发时最常碰到的需求列表。Compose 把 Android 传统的 RecyclerView Adapter 这套写法换成了全新的声明式 API核心是 LazyColumn 和 LazyRow。你不用再写 ViewHolder、不用手动 diff、不用关心 LayoutManager数据一变界面自动跟着变。这篇文章会从环境准备开始给出一个完整可运行的列表页面示例覆盖基础列表、点击事件、多类型 Item、性能优化、常见错误排查最后补一份和 RecyclerView 的对比。无论你是 Compose 新手还是正准备把项目里的 RecyclerView 迁移到 Compose都可以直接照着抄。1. Jetpack Compose 列表核心能力速览能力项说明核心组件LazyColumn、LazyRow、LazyVerticalGrid、LazyHorizontalGrid编程模型声明式 UIKotlin DSL 描述界面数据绑定方式直接传入 List状态变化自动重组重绘替代对象RecyclerView、Adapter、ViewHolder、LayoutManager列表项创建item() 单条、items() 集合、itemsIndexed() 带下标多类型 Item使用 sealed class when 表达式分发性能机制懒加载仅组合可见区域 Item稳定化机制key 指定唯一标识contentType 区分类型官方组件Material3 主题下的 ListItem、Card 等可直接复用运行要求Android Studio Kotlin Compose 依赖上手难度低熟悉 Kotlin 即可从这张表能看到Compose 列表的核心思路不是“复用 View”而是“用当前数据组合出当前界面”。数据不再是一份要喂给 Adapter 的模型而是 UI 的唯一数据源这是理解 Compose 列表的关键。2. 适用场景与使用边界2.1 适合谁正在学习 Compose 的安卓开发者列表是 Compose 必学的基础章节这个项目的定位就是面向初学者的中文讲解适合按章节顺序刷。维护传统 View 体系项目的开发者如果项目正在从 RecyclerView 迁移到 Compose列表迁移是优先级最高、收益最明显的一步。需要多端复用技能的开发者Compose 的声明式 API 与 SwiftUI、Flutter 有思维上的相似性学会列表写法后再看其他声明式框架会轻松很多。2.2 能解决什么问题去掉 Adapter、ViewHolder 的模板代码列表代码量明显下降。数据变化后不需要手动通知刷新状态驱动 UI 自动更新。列表项可以像普通 Composable 一样自由组合支持嵌套滚动、带动画、加点击水波纹。在 LazyVerticalGrid 下可以快速实现网格布局不需要 GridLayoutManager。2.3 不适合什么场景超大列表且需要离线分页虽然 LazyColumn 支持懒加载但自己管理分页、缓存、重试并不省心建议接 Paging 3。复杂的自定义滚动行为比如仿抖音的信息流、需要同步位移的双列表、非常规 LayoutManager 效果Compose 现阶段仍然要引入底层手势系统或自绘成本高于 RecyclerView 方案。项目以 Java 为主且团队不熟悉 KotlinCompose 是 Kotlin 优先的框架Java 项目迁移 Compose 虽然可行但代码写起来会别扭列表部分收益会打折。2.4 使用边界与合规提醒列表本身只是 UI 组件不涉及内容生成但要特别留意列表里展示的内容来源。如果做资讯类 App、视频列表、用户头像昵称列表涉及图片、文本、音乐、视频等素材务必确认素材版权和用户授权。做 UGC 内容列表时还要考虑内容审核、隐私保护、用户协议等合规要求。技术本身没有边界问题但内容分发场景必须在产品层面做好合规设计。3. Jetpack Compose 列表开发环境准备3.1 推荐开发环境项推荐配置开发工具Android Studio保持官方最新稳定版语言Kotlin 1.8 以上最低 APIAPI 21 / 26 取决于 Compose 版本构建工具Android Gradle Plugin Kotlin Gradle PluginCompose 依赖Compose BOM 管理设备真机或模拟器均可推荐 API 30 以上测试这里不写死具体版本号因为 Compose 版本迭代很快。打开 Android Studio 新建项目时直接选Empty Activity并勾选Jetpack ComposeIDE 会自动生成一份能跑的 Compose 配置。3.2 确认 build.gradle 配置新建项目后打开模块的build.gradle.kts确认包含以下内容android { buildFeatures { compose true } } dependencies { implementation(platform(androidx.compose:compose-bom:2024.06.00)) implementation(androidx.compose.ui:ui) implementation(androidx.compose.material3:material3) implementation(androidx.compose.ui:ui-tooling-preview) debugImplementation(androidx.compose.ui:ui-tooling) }compose-bom里的版本号可以替换成你本机能拉取到的最新版本。重点是compose true这个开关它确保 Kotlin 编译器插件能处理Composable注解。3.3 验证环境在 MainActivity 里先运行一个最简单的 Composableclass MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text(Compose OK) } } }运行到模拟器或真机上能看到 Compose OK 说明环境没问题。接着再开始写列表。4. 基础列表实现ColumnverticalScroll 与 LazyColumn4.1 小数据量写法Column verticalScroll如果列表项很少比如 5 到 10 个固定条目可以用Column配合verticalScrollComposable fun SimpleColumnList() { val items listOf(苹果, 香蕉, 橘子, 葡萄, 西瓜) Column( modifier Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) ) { items.forEach { item - Text( text item, modifier Modifier .fillMaxWidth() .padding(16.dp) ) } } }这种写法会一次性把所有 Item 全部组合出来。数据量小没问题数据量一大所有 Item 同时进组合滑动时也不会有复用机制性能就会迅速恶化。所以列表项多的时候应该使用 LazyColumn。4.2 推荐写法LazyColumnLazyColumn 是 Compose 里的垂直列表容器它只组合当前可见区域的 Item类似 RecyclerView 的 ViewHolder 复用机制但开发者不需要写任何 AdapterComposable fun FruitList() { val fruits listOf(苹果, 香蕉, 橘子, 葡萄, 西瓜) LazyColumn( modifier Modifier.fillMaxSize(), contentPadding PaddingValues(16.dp), verticalArrangement Arrangement.spacedBy(8.dp) ) { items(fruits) { fruit - Text( text fruit, modifier Modifier .fillMaxWidth() .background(Color.LightGray) .padding(16.dp) ) } } }这里有几个点值得注意contentPadding控制整个列表的上下左右内边距它不会滚动到顶部时贴边符合 Material 风格。verticalArrangement控制 Item 之间的间距用Arrangement.spacedBy(8.dp)比给每个 Item 加padding更统一。items(fruits)会为每个fruit调一次后面的 Composable这就是 Compose 的声明式“遍历集合生成 UI”。启动后可以看到列表可以滚动Item 之间间距一致顶部和底部留有统一的内边距。这是所有 Compose 列表的基础形态。5. LazyColumn 常用 API 与参数详解5.1 item 单条插入当列表里需要插入“标题”“加载更多”“广告位”这类非集合项时使用item函数LazyColumn { item { Text(header) } items(fruits) { fruit - Text(fruit) } item { Text(footer) } }item不会遍历集合而是直接组合一个 Composable。你可以在任意位置插入任意多个item。5.2 itemsIndexed 带下标当你需要知道当前 Item 的下标时使用itemsIndexedComposable fun IndexedList() { val fruits listOf(苹果, 香蕉, 橘子) LazyColumn { itemsIndexed(fruits) { index, fruit - Text($index - $fruit) } } }这里的index是当前 Item 在集合中的位置。如果列表支持删除中间项推荐用数组自身的数据作为 key 而不是 index否则删除项后容易产生抖动。5.3 items 的 key 参数items可以传入key参数用来给每个 Item 指定稳定的唯一标识LazyColumn { items( items fruits, key { it } ) { fruit - Text(fruit) } }key的作用是帮助 Compose 在列表数据变化时识别哪些 Item 是新增、删除、移动的。尤其是列表项带有输入框、开关、图片异步加载状态时没有 key 会导致状态复用错乱。5.4 stickyHeader 吸顶效果使用 LazyColumn 实现分组吸顶标题时可以用stickyHeaderLazyColumn { stickyHeader { Text(今日水果) } items(fruits) { fruit - Text(fruit) } }这个 API 不需要额外引入实验注解在官方 Compose Foundations 库里随 LazyColumn 一起提供。实际看到的效果是标题滚到顶部后停住直到下一组标题把它顶走。6. 列表项点击与状态管理6.1 给 Item 加点击事件列表项最常见的是点击跳转或选中。直接给 Composable 加clickable修饰符Composable fun ClickableList() { val fruits remember { mutableStateListOf(苹果, 香蕉, 橘子) } var selected by remember { mutableStateOfString?(null) } LazyColumn { items(fruits) { fruit - val isSelected selected fruit Text( text fruit, modifier Modifier .fillMaxWidth() .clickable { selected fruit } .background( if (isSelected) Color.Cyan else Color.Transparent ) .padding(16.dp) ) } } }selected是 State 变量。点击某个 Itemselected更新Compose 自动重组列表被选中的 Item 背景变为青色。这里不需要像 RecyclerView 那样调用notifyDataSetChanged。6.2 状态提升把点击回调抽出去Item 内部不要持有数据仓库或导航逻辑正确的做法是把点击回调提升到父级Composable fun FruitItem( fruit: String, onClick: (String) - Unit ) { Text( text fruit, modifier Modifier .fillMaxWidth() .clickable { onClick(fruit) } .padding(16.dp) ) } Composable fun FruitListPage() { val fruits listOf(苹果, 香蕉, 橘子) LazyColumn { items(fruits) { fruit - FruitItem(fruit fruit, onClick { name - // 在这里处理跳转或业务逻辑 println(点击了 $name) }) } } }这样做的好处是 Item 足够干净便于复用、测试和 Preview 预览。6.3 滑动删除Material 3 提供SwipeToDismissBox在列表里配合 LazyColumn 使用可以实现侧滑删除val fruits remember { mutableStateListOf(苹果, 香蕉, 橘子) } LazyColumn { items(fruits) { fruit - val dismissState rememberSwipeToDismissBoxState() SwipeToDismissBox( state dismissState, backgroundContent { Box(Modifier.fillMaxSize().background(Color.Red)) } ) { Text( text fruit, modifier Modifier .fillMaxWidth() .padding(16.dp) ) } } }实际使用时要在dismissState变化后把数据从fruits里移除否则 Item 滑走了但数据还在下次重组又会回来。7. 多类型 Item 列表实现实际业务里列表很少只有一种样式。常见的有Banner、商品卡片、小标题、普通行、加载更多不同类型展示不同布局。7.1 数据模型用 sealed class 定义列表数据类型sealed class ListItem { data class BannerItem(val imageUrl: String) : ListItem() data class ProductItem(val title: String, val price: String) : ListItem() data class SectionTitle(val title: String) : ListItem() data class LoadingItem(val message: String) : ListItem() }7.2 渲染逻辑Composable fun MultiTypeList(items: ListListItem) { LazyColumn { items( items items, key { it.hashCode() }, contentType { it::class.java.simpleName } ) { item - when (item) { is ListItem.BannerItem - BannerView(item.imageUrl) is ListItem.ProductItem - ProductView(item.title, item.price) is ListItem.SectionTitle - SectionTitleView(item.title) is ListItem.LoadingItem - LoadingView(item.message) } } } }contentType参数告诉 Compose 当前 Item 的类型。当列表滚动时Compose 会尝试复用相同 contentType 的 Item 组合。如果不传Compose 默认所有 Item 是可以互相复用的类型遇到多类型列表会影响性能。7.3 运行效果界面会按照items传入的顺序渲染不同布局。Banner 区域是一整块大图SectionTitle 是分组标题ProductItem 是普通商品卡片LoadingItem 是底部加载条。数据顺序由上层决定UI 层只负责分发。8. 列表性能优化与状态保持8.1 使用稳定的 keykey 是 Compose 列表性能的第一道保险。列表变化时key 能帮助 Compose 尽量少的重组 Item。推荐使用数据库主键或服务端返回的唯一 ID不要用 index。items( items products, key { product - product.id } ) { product - ProductView(product) }8.2 避免 Item 内部持有高频状态Compose 的重组范围是“用到 State 的最小作用域”。Item 里引入大量 State列表滚动时每个 Item 的重组成本都会变高。合理做法是把稳定的配置放外层把可变化的交互状态放到 Item 内部用remember包起来。比如每个 Item 都有一个展开/收起状态Composable fun ExpandableItem(product: Product) { var expanded by remember { mutableStateOf(false) } Column { Row(Modifier.clickable { expanded !expanded }) { Text(product.title) } if (expanded) { Text(product.description) } } }8.3 数据量很大的分页思路LazyColumn 本身不会自动加载下一页。要在滚动到底部附近时触发加载可以使用derivedStateOf监听最后一个可见 Itemval listState rememberLazyListState() val shouldLoadMore by remember { derivedStateOf { val lastVisibleItem listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 lastVisibleItem listState.layoutInfo.totalItemsCount - 3 } } LaunchedEffect(shouldLoadMore) { if (shouldLoadMore) { viewModel.loadMore() } } LazyColumn(state listState) { items(viewModel.items) { item - ... } }这个写法适合中小型列表。生产环境数据量很大时建议引入 Paging 3 库Compose 官方对 Paging 3 有专门支持。9. Jetpack Compose 列表与 RecyclerView 对比对比项RecyclerViewCompose LazyColumn布局描述XML 定义 Item 布局Composable 函数描述 Item数据绑定ViewHolder onBindViewHolderdata 直接进入 Composable 参数数据更新notifyDataSetChanged / DiffUtil状态变化自动重组复用机制ViewHolder 复用组合作用域复用多样式getItemViewType 多 ViewHoldersealed class when 分发间距ItemDecorationArrangement.spacedBy / contentPadding列表头尾手动加 Header/Footer Typeitem() 直接插入学习成本Adapter 体系较重Kotlin DSL 更直观异步图片需要自定义或引入库同样引入 Coil 等库但写法更简洁一个很直观的差别RecyclerView 里添加 Header 和 Footer要么写多个 ViewType要么继承 RecyclerView.Adapter 自己维护列表逻辑LazyColumn 里就是两个item {}的事。// RecyclerView 风格伪代码需要判断 position 对应的类型 override fun getItemViewType(position: Int): Int { return when (position) { 0 - TYPE_HEADER else - TYPE_ITEM } } // Compose 风格直接插入 LazyColumn { item { Header() } items(list) { item - ItemView(item) } }如果你正在维护老项目建议先在业务简单、数据量小的页面上试水 Compose 列表跑通后再替换复杂列表。10. Jetpack Compose 列表常见问题与排查问题现象可能原因排查方式解决方案列表不更新传入的是普通 List没有 State 包裹检查数据源是否用 mutableStateListOf / StateFlow改用状态容器持有数据滚动卡顿、掉帧Item 内存在高开销副作用检查 Item 内是否做了大量计算或数据库访问把计算移到 ViewModel用 Flow 驱动Item 状态错乱key 缺失或 key 不稳定检查 items 是否传入 key使用唯一 ID 作为 key多类型 Item 白屏sealed class when 分支不完整检查 when 是否覆盖所有子类补全分支或加 else嵌套滚动冲突LazyColumn 内嵌 LazyColumn观察内层滚动事件使用固定高度子列表或改用 LazyVerticalGrid 分块点击无响应父容器拦截了事件检查 Modifier 顺序和嵌套组件调整 clickable 的位置列表底部展示不全contentPadding 设置错误检查末尾是否有间隔合理设置 contentPadding 和 Arrangement页面爆内存一次性加载大图片列表检查是否用了大图加载使用 Coil 或 Glide 做内存缓存和采样Compose 编译器报错版本与 Kotlin 不匹配检查 composeOptions 配置升级 Compose 编译器插件或降低 BOM 版本删除项时闪跳key 用 index删除中间项后列表无法识别改为使用数据唯一 ID这里面最容易被新手忽略的是第一行列表数据必须放在状态容器里。Compose 不会主动监听一个普通MutableList的变化你用val list mutableListOf(...)传给 LazyColumn然后list.add(...)界面是不会变的。必须使用mutableStateListOf、StateFlow.collectAsState()或LiveData.observeAsState()才能驱动 UI 更新。11. 项目实战建议11.1 页面结构建议一个标准的 Compose 列表页至少要有四层ViewModel数据源暴露 StateFlow ↓ Screen / Page Composable持有列表 State负责重组调度 ↓ LazyColumn容器负责懒加载 ↓ Item Composable单个 Item 展示Composable fun ProductListScreen(viewModel: ProductViewModel viewModel()) { val products by viewModel.products.collectAsState() LazyColumn( contentPadding PaddingValues(16.dp), verticalArrangement Arrangement.spacedBy(12.dp) ) { items( items products, key { it.id } ) { product - ProductCard(product product) } } }11.2 第一版先跑最小功能不要一上来就追求多类型、动画、滑动删除。先把一个普通文本列表跑通再逐步加入点击、状态、复杂 Item、无限滚动。每加一层功能都先确认上一层的稳定性。11.3 维护一套可运行的示例项目列表这块代码很适合单独抽出一个模块或示例页面。当 Compose 版本升级导致 API 变化时用一个最小的列表示例项目验证新版本是否兼容成本远低于在大型项目里排查。11.4 涉及列表内容的合规要求如果列表是图片、音频、视频、用户头像等内容的聚合务必确认每一项素材的来源合法。商业项目里UGC 内容列表还需要考虑内容审核逻辑、用户隐私、数据存储合规。这些都是产品能不能上线的前提优先级高于 UI 实现。12. 总结与下一步这个项目最值得先跑通的功能是 LazyColumn 的基础写法用items渲染一个集合加上key再加上contentType一个可用的列表就完成了。最容易踩的坑有两个一个是列表数据不是 State导致数据变更后 UI 不刷新另一个是多类型 Item 漏了 contentType滚动性能和状态保持都会出问题。后续可以继续扩展的方向用 Paging 3 接入无限滚动分页列表。用 LazyVerticalGrid 做网格列表。用animateItem()给 Item 增删加动画。用LazyColumnstickyHeader做分组通讯录列表。把现有 RecyclerView 页面一个接一个迁移到 Compose。建议直接跟着这篇文章搭一个最简 Demo然后在自己的真实业务数据上验证这样对 Compose 声明式列表的体感会比只看文档深刻得多。
返回列表