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

资讯详情

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

Jetpack Compose列表完全指南:LazyColumn核心原理与实战

Jetpack Compose列表完全指南:LazyColumn核心原理与实战 自接触 Jetpack Compose 这几个月以来我最大的感受是列表在 Compose 里的写法和传统 View 体系完全不是一个路子。你习惯了 RecyclerView Adapter 后第一次打开 LazyColumn 可能会觉得“就这么点代码”但真要处理动态列表、点击刷新、多类型 item、状态丢失这些场景又会发现有很多隐藏的知识点。这篇教程是我整理的“Jetpack Compose 安卓声明式 UI 开发”系列第 8 篇专门讲列表。我把整个内容拆成核心概念 → 环境准备 → 基础组件 → 全流程实战 → 进阶技巧 → 常见问题 → 工程建议从头捋一遍。无论你是刚接触 Compose 的新手还是想系统补一补声明式列表知识的开发者这篇都可以直接作为学习资料收藏。1. 为什么“列表”在 Compose 里值得单独讲1.1 声明式 UI 与传统 View 的本质区别先想想以前我们怎么做列表。在传统View体系中开发一个列表通常需要三步在 XML 里写一个RecyclerView。新建一个Adapter类实现onCreateViewHolder、onBindViewHolder、getItemCount。把数据列表交给 Adapter再通过notifyDataSetChanged()或DiffUtil通知界面刷新。这套模式叫命令式 UI。核心逻辑是你手动控制“View 什么时候创建”“数据什么时候绑定”“变化怎么通知界面”。而 Jetpack Compose 是声明式 UI。声明式的意思是你只需要描述“当前状态应该呈现成什么样子”剩下的创建、更新、销毁交给框架。列表也不例外。LazyColumn { items(articleList) { article - ArticleItem(article) } }这段代码表达的含义是把 articleList 逐条渲染成 ArticleItem。如果 articleList 变了LazyColumn 内部会自动对比、更新、复用可组合项不需要你写 Adapter也不需要手动调用刷新方法。这就是声明式和命令式最核心的区别你不再告诉界面“怎么变”而是告诉它“现在是什么”。1.2 为什么列表在 Compose 里是一个独立主题可能你会问列表不就是把几个 Text 排成一列吗为什么值得单独开一篇因为列表在移动应用里承担的是“大量数据 滑动浏览 动态刷新”的职责它对性能、内存、状态保持的要求远高于普通静态布局。Compose 作为声明式框架它解决列表问题的方式是 LazyColumn / LazyRow但“懒加载”这个机制背后有很多容易被忽略的细节列表项什么时候被创建什么时候被销毁滚动出屏幕后item 状态还在不在数据变了怎么精准更新某一条而不是全量重绘多类型 item 怎么声明加载更多、分页、空状态怎么优雅处理这些问题都不是“写一行 LazyColumn”能直接回答的所以我把列表单独拎出来做一集并且尽量把核心用法和工程经验都覆盖到。本集内容是系列教程中的第 8 篇假设你已经掌握了 Compose 的基础环境搭建、Composable函数、常见的布局组件Column、Row、Box和状态管理的基本用法。如果你刚入门建议先熟悉前面的基础内容再回来看这一篇。2. 环境准备把 Compose 项目跑起来2.1 开发环境与版本说明写 Compose 列表之前我们先把环境跑通。下面以常见的 Android Studio 开发环境为例。要注意具体版本号需要根据你的项目实际情况调整。Compose 从发布到现在迭代很快如果你是从零搭建项目建议直接在 Android Studio 新建工程时选择 “Empty Activity”并且在模板里勾选 Compose 支持如果你是给已有项目加 Compose则要确认 Gradle、Kotlin、Compose 编译器之间的版本匹配。简单来说Compose 项目通常需要满足Android Studio 版本足够新能正常创建 Compose 模板项目。Kotlin 版本与 Compose 编译器插件版本兼容。使用 Compose BOMBill of Materials统一管理 Compose 各组件版本而不是手动一个个写版本号。2.2 添加 Compose 依赖假设你是在已有 Android 项目里接入 Compose核心依赖大致如下。这里以 Kotlin DSLbuild.gradle.kts为例// 文件路径app/build.gradle.kts android { buildFeatures { compose true } composeOptions { // kotlinCompilerExtensionVersion 需要和你项目的 Kotlin 版本匹配 // 示例写法实际版本请按官方文档调整 kotlinCompilerExtensionVersion 1.5.10 } } dependencies { val composeBom platform(androidx.compose:compose-bom:2024.06.00) implementation(composeBom) implementation(androidx.compose.ui:ui) implementation(androidx.compose.ui:ui-graphics) implementation(androidx.compose.ui:ui-tooling-preview) implementation(androidx.compose.material3:material3) implementation(androidx.compose.foundation:foundation) // 可选生命周期相关组件 implementation(androidx.lifecycle:lifecycle-runtime-ktx:2.8.3) implementation(androidx.activity:activity-compose:1.9.0) debugImplementation(androidx.compose.ui:ui-tooling) }如果是 Groovy DSLbuild.gradle写法类似dependencies { implementation platform(androidx.compose:compose-bom:2024.06.00) implementation androidx.compose.ui:ui implementation androidx.compose.material3:material3 }这里用 BOM 的好处是你只需要指定一次 BOM 版本下面几个androidx.compose.*库的版本都会由 BOM 统一对齐避免冲突。为什么需要 Foundation 库LazyColumn、LazyRow 都是androidx.compose.foundation.lazy包下的组件所以必须引入foundation依赖。2.3 常见构建报错处理刚搭环境时最容易遇到的是版本不匹配“Compose compiler plugin 需要 Kotlin 版本 X”说明composeOptions.kotlinCompilerExtensionVersion与 Kotlin 版本不对应按错误提示调整。“Unable to find annotation processor”通常和 Kotlin 或插件配置缺失有关检查是否启用了kotlin(plugin.compose)。混淆模式下列表点击失效一般和 release 混淆规则有关需要保留 Compose 相关类但这属于后期问题。环境跑通之后我们就可以开始正题了。3. Compose 列表核心组件LazyColumn 与 LazyRow3.1 LazyColumn 与 LazyRow 是什么LazyColumn是 Compose 里用于“垂直方向”懒加载列表的组件对应传统 View 体系中的RecyclerView。它有几个关键特点懒加载只组合那些需要显示在屏幕上的 item以及少量预加载的缓冲 item。列表很长时不会一次性创建所有 item。自动回收复用item 滑出屏幕后Compose 会收回它的组合资源滑回来时重新创建。声明式 API在LazyColumn的content作用域里声明 item 的组成而非通过 adapter 绑定。LazyRow则是横向版本的懒加载列表适合做横滑卡片、图片轮播等场景。基础用法Composable fun SimpleListScreen() { val items listOf(Android, Kotlin, Compose, LazyColumn) LazyColumn( modifier Modifier.fillMaxSize() ) { items(items) { name - Text( text name, modifier Modifier .fillMaxWidth() .padding(16.dp), fontSize 18.sp ) } } }看到没有没有 Adapter没有 ViewHolder没有getItemCount()。只需要一个items()作用域函数把一个数据列表传进去列表项怎么展示由一个普通 Composable 描述。3.2 item、items、itemsIndexed 的区别很多初学者会在LazyColumn的 content 作用域里被三个函数搞混。1.item添加单个列表项LazyColumn { item { Text(这是一个单独的表头) } item { Text(这是另一个单独项) } }适合固定数量、内容独立的列表项比如列表头部的 Banner、中间的广告位、底部“加载中”提示。2.items传入一个列表逐条渲染LazyColumn { items(articleList) { article - ArticleItem(article) } }这里的items是一个扩展函数它接收一个ListT并为每个元素调用一次后面的渲染 lambda。这是最常用的写法。3.itemsIndexed渲染时同时拿到下标LazyColumn { itemsIndexed(articleList) { index, article - Text(第 $index 条${article.title}) } }适合需要显示序号、间隔线、奇偶行背景等场景。items还有一个重载可以接收带 key 的数据结构比如LazyColumn { items( items articleList, key { it.id } ) { article - ArticleItem(article) } }这个key参数非常关键我们下一节细说。3.3 key 参数为什么列表项必须有稳定身份在 Compose 中LazyColumn 会通过 item 在列表中的位置来识别它。但如果你的列表是动态的比如用户删除了一条、或者拉到新数据后插入了新条目那么位置会变化Compose 就需要一种方式判断“哪个 item 还是原来的那个”。key就是干这个用的。LazyColumn { items( items articleList, key { article - article.id } ) { article - ArticleItem(article) } }当key稳定且唯一时Compose 能保持 item 的状态——比如列表项内的输入框内容、滚动位置、开关状态。如果不加key当列表在头部插入数据时Compose 可能把旧的 item 状态错配到新的数据上。实际项目里列表数据一定要有稳定唯一的 id最好用数据库主键或服务端返回的 id不要再随手用 index。关于 index 的一个更隐蔽的问题如果 list 是mutableListOf并且在组合过程中被修改会导致运行时崩溃或者项乱跳。后面常见问题里会提到。4. 完整实战做一个文章列表 App下面我们从头写一个文章列表界面把上面讲到的内容落到代码里。为了便于理解我们拆成几步。4.1 设计数据结构先定义新闻/文章的数据模型// 文件路径app/src/main/java/com/example/listdemo/model/Article.kt package com.example.listdemo.model data class Article( val id: Int, val title: String, val summary: String, val author: String, val readCount: Int )这里我们使用data class因为 Compose 列表经常需要比较数据内容data class会自动实现equals()和hashCode()方便我们后续做状态判断。再写一个模拟数据源方便测试// 文件路径app/src/main/java/com/example/listdemo/data/MockData.kt package com.example.listdemo.data import com.example.listdemo.model.Article object MockData { fun getArticles(): ListArticle { return List(50) { index - Article( id index 1, title Jetpack Compose 系列教程第 ${index 1} 篇, summary 这是第 ${index 1} 篇文章的摘要用来展示 Compose 列表的懒加载效果。, author if (index % 2 0) 张三 else 李四, readCount (1000 index * 35) ) } } }这里用List(50)快速生成 50 条数据用来验证 LazyColumn 的懒加载性能。4.2 定义列表项 UI列表项的 UI 我们用Card包一层方便观察视觉效果。// 文件路径app/src/main/java/com/example/listdemo/ui/ArticleItem.kt package com.example.listdemo.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.example.listdemo.model.Article Composable fun ArticleItem( article: Article, onClick: (Article) - Unit ) { Card( onClick { onClick(article) }, modifier Modifier .fillMaxWidth() .padding(horizontal 16.dp, vertical 8.dp), shape RoundedCornerShape(12.dp) ) { Column( modifier Modifier.padding(16.dp) ) { Text( text article.title, style MaterialTheme.typography.titleMedium ) Text( text article.summary, style MaterialTheme.typography.bodyMedium, modifier Modifier.padding(top 6.dp) ) Row( modifier Modifier .fillMaxWidth() .padding(top 10.dp), horizontalArrangement Arrangement.SpaceBetween ) { Text( text 作者${article.author}, style MaterialTheme.typography.labelMedium ) Text( text 阅读 ${article.readCount}, style MaterialTheme.typography.labelMedium ) } } } }在 Compose Material3 中Card可以直接传入onClick参数变成一个可点击组件不需要额外包一层Modifier.clickable。这样代码更简洁也保留了 Card 本身的阴影和圆角效果。4.3 组装 LazyColumn现在把数据源和列表项组合进一个页面。// 文件路径app/src/main/java/com/example/listdemo/ui/ArticleListScreen.kt package com.example.listdemo.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBar import com.example.listdemo.data.MockData import com.example.listdemo.model.Article OptIn(ExperimentalMaterial3Api::class) Composable fun ArticleListScreen( onArticleClick: (Article) - Unit ) { val articles MockData.getArticles() Scaffold( topBar { TopAppBar( title { Text(Compose 列表实战) } ) } ) { innerPadding - LazyColumn( modifier Modifier .fillMaxSize() .padding(innerPadding), contentPadding PaddingValues(vertical 8.dp) ) { items( items articles, key { article - article.id } ) { article - ArticleItem( article article, onClick onArticleClick ) } } } }到这里一个最基本的列表页面就完成了。contentPadding 的作用它给 LazyColumn 整体内容区域加内边距但不会影响列表的滚动范围。换句话说列表滑到最底部时仍能看到底部留白不会贴着屏幕边缘。4.4 点击交互与状态更新实际项目中列表往往不是静态的。比如点击收藏按钮要更新状态、点击 item 要跳转详情。我们给 ArticleItem 增加一个“收藏”状态Composable fun ArticleItem( article: Article, isFavorite: Boolean, onClick: (Article) - Unit, onToggleFavorite: (Article) - Unit ) { Card( onClick { onClick(article) }, modifier Modifier .fillMaxWidth() .padding(horizontal 16.dp, vertical 8.dp) ) { Column(modifier Modifier.padding(16.dp)) { Text(text article.title, style MaterialTheme.typography.titleMedium) Text(text article.summary, style MaterialTheme.typography.bodyMedium) Row( modifier Modifier .fillMaxWidth() .padding(top 10.dp), horizontalArrangement Arrangement.SpaceBetween ) { Text(text 作者${article.author}, style MaterialTheme.typography.labelMedium) Text( text if (isFavorite) ★ 已收藏 else ☆ 收藏, modifier Modifier.clickable { onToggleFavorite(article) }, color if (isFavorite) Color(0xFFFF9800) else Color.Gray ) } } } }在列表层管理状态Composable fun ArticleListScreen() { val articles remember { MockData.getArticles() } val favoriteIds remember { mutableStateOf(setOfInt()) } LazyColumn { items( items articles, key { it.id } ) { article - ArticleItem( article article, isFavorite article.id in favoriteIds.value, onClick { /* 跳转详情 */ }, onToggleFavorite { target - favoriteIds.value if (target.id in favoriteIds.value) { favoriteIds.value - target.id } else { favoriteIds.value target.id } } ) } } }这里维护了一个favoriteIds集合。点击收藏时通过不可变集合的/-操作生成新集合触发重组。只有那条 item 的isFavorite发生变化Compose 会自动重组显示文字。注意不要直接修改favoriteIds.value内部内容比如add()因为 Compose 只有在状态对象被赋新值时才认为状态发生了变化。4.5 横滑列表 LazyRow 示例除了竖向列表横向列表也非常常用。比如首页顶部的“热门标签”或“推荐话题”。下面是一个简单的 LazyRow 示例Composable fun HotTagRow(tags: ListString) { LazyRow( contentPadding PaddingValues(horizontal 16.dp), horizontalArrangement Arrangement.spacedBy(8.dp) ) { items(tags) { tag - Surface( shape RoundedCornerShape(50), color MaterialTheme.colorScheme.secondaryContainer, modifier Modifier.padding(vertical 4.dp) ) { Text( text tag, modifier Modifier.padding(horizontal 14.dp, vertical 6.dp), style MaterialTheme.typography.labelMedium ) } } } }horizontalArrangement Arrangement.spacedBy(8.dp)可以让每个横向 item 之间自动加 8dp 间距比在每个 item 上单独加 padding 更优雅。5. Compose 列表进阶技巧5.1 不同 item 类型混合一个真实 App 的列表往往不是单一类型而是头部 Banner、普通文章、推荐卡片、底部加载状态混在一起的。Compose 里有两种常见做法。做法一在 LazyColumn 中手动组合不同类型LazyColumn { item { Banner() } items(articleList, key { it.id }) { article - ArticleItem(article) } item { LoadingFooter() } }这种方式适合类型不多、顺序相对固定的场景。做法二用 sealed class 抽象统一数据源如果列表数据来自多个接口、顺序动态变化我倾向于定义统一的数据模型再在渲染时做分支。sealed interface ListItem { data class ArticleItemData(val article: Article) : ListItem data class BannerItemData(val imageUrl: String) : ListItem data class AdItemData(val adText: String) : ListItem }然后在 LazyColumn 中通过 when 分支LazyColumn { items(items mixedList, key { it.hashCode() }) { item - when (item) { is ListItem.ArticleItemData - ArticleItem(item.article) is ListItem.BannerItemData - Banner() is ListItem.AdItemData - Text(item.adText) } } }注意使用hashCode()作为 key 不够稳定生产环境还是建议给每个 item 一个唯一 id 字段尤其在列表会动态变化的情况下。5.2 加载更多与分页思路LazyColumn 提供了一个非常在意的监听时机rememberLazyListState()可以拿到列表滚动状态配合derivedStateOf判断是否需要加载更多。val listState rememberLazyListState() val shouldLoadMore by remember { derivedStateOf { val lastVisibleItem listState.layoutInfo.visibleItemsInfo.lastOrNull() lastVisibleItem?.index ! null lastVisibleItem.index listState.layoutInfo.totalItemsCount - 3 } } LaunchedEffect(shouldLoadMore) { if (shouldLoadMore) { viewModel.loadMore() } } LazyColumn( state listState ) { items(articles) { article - ArticleItem(article) } }这段代码的思路当最后可见 item 的 index 接近列表总数时触发加载更多。derivedStateOf可以避免在每次滚动时都触发不必要的重组。5.3 空状态与加载状态列表数据为空时直接展示一个空白页面体验很差。我们可以用if/else控制三种状态when { isLoading - LoadingView() isEmpty - EmptyView() else - LazyColumn { items(articles) { ... } } }LoadingView显示 CircularProgressIndicatorEmptyView显示一张占位图和“暂无数据”文案。这样结构清晰也方便测试。5.4 列表性能观察点Compose 列表的性能问题多数不是 LazyColumn 本身慢而是单个 item 内部太复杂。重点关注item 里是否做了耗时操作item 里是否使用了过大的图片每个 item 是否存在不必要的重组关于第三点可以用Layout Inspector或Compose 编译器插桩观察重组次数。如果单个 item 频繁重组可以检查状态读取范围是否过大以及是否缺少稳定 key。6. 常见问题与排查思路6.1 问题排查表格问题现象常见原因解决思路LazyColumn 滚动后数据错乱列表项 key 缺失或重复为 items 提供基于 id 的稳定 key列表顶部插入数据后 item 状态错乱没有使用稳定 key使用唯一 id 作为 key修改列表数据后界面不刷新直接修改了可变集合内部内容创建新集合赋值给 mutableStateOf页面滑动卡顿item 内部布局复杂、图片过大简化 item UI、使用图片懒加载库运行时 ConcurrentModificationException组合期间修改了 MutableList使用不可变快照或在非组合阶段修改列表点击无响应Card 的 onClick 没写或者被子组件拦截检查点击事件绑定、子组件可点击属性底部间距被遮挡没有设置 contentPadding给 LazyColumn 设置 contentPadding 或 bottomBar 边距6.2 三个高频坑的详细说明坑 1动态列表没有 key 导致状态错乱这是用 LazyColumn 最容易踩的坑。来看一个错误示例// 不要这样做 LazyColumn { itemsIndexed(articles) { index, article - ArticleItem( article article, isFavorite favoriteIds.contains(article.id) ) } }不使用 key 时Compose 按 index 映射列表项。如果用户在列表头部删了一条后面的每一项都会往前挪一位原本的收藏状态可能迁移到错误的数据上。解决方式给items指定 key。坑 2Compose 列表项内部状态丢失比如 item 内有一个输入框滑出屏幕再滑回来输入内容不见了。原因通常是 item 被回收后组合状态没有保存。解决方式使用rememberSaveable保存状态。把输入内容提升到 ViewModel 或父级状态中而不是只存在 item 内部。确保 key 稳定使 Compose 能保存 item 的状态。坑 3Compose 编译器版本不匹配导致构建失败这个在环境准备那节提到过。Compsoe 编译器插件并不是独立版本它和 Kotlin 版本有一一对应关系。如果升级 Kotlin忘记升级composeOptions.kotlinCompilerExtensionVersion构建就会报错。解决方式参考官方 Compose 与 Kotlin 兼容性对照表保持版本一致。7. 最佳实践与工程建议7.1 代码组织与命名列表页面我建议按“数据模型 → 列表项 UI → 页面组装”三层拆分model/ 数据类 Article.kt data/ 数据源 MockData.kt / Repository ui/components/ ArticleItem.kt ui/screens/ ArticleListScreen.kt这样每个文件职责单一。列表项 UI 只负责展示页面负责状态管理数据源负责数据获取。初学者最容易犯的错误是把数据加载、状态管理、列表项 UI 全写在一个 Composable 里导致一个函数上百行后期很难维护。7.2 状态管理对于列表这类有大量数据交互的场景我推荐使用ViewModel配合StateFlow来管理列表数据而不是把所有状态都塞进 Composable。class ArticleListViewModel : ViewModel() { private val _articles MutableStateFlowListArticle(emptyList()) val articles: StateFlowListArticle articles.asStateFlow() fun loadArticles() { viewModelScope.launch { val result repository.getArticles() _articles.value result } } }UI 层用collectAsStateWithLifecycle()收集状态Composable fun ArticleListScreen(viewModel: ArticleListViewModel) { val articles by viewModel.articles.collectAsStateWithLifecycle() LazyColumn { items(articles, key { it.id }) { article - ArticleItem(article) } } }这样做的优点数据与 UI 解耦。配置变更旋转屏幕时列表数据不丢失。方便后续接入分页、缓存、网络重试等能力。7.3 列表数据的不可变性在 Compose 中不可变数据是状态管理的核心原则。线程切换、重组发生时不可变数据可以避免很多“改了一半”的中间态问题。具体到列表对外暴露的列表尽量使用ListT而不是MutableListT。数据更新时创建新列表比如list newItem或者list.filter { ... }。不要在 Composable 内部直接修改数据源。这不仅是 Compose 的规定也是函数式编程思路的体现数据流清晰、可预测、更容易 Debug。7.4 如何在简历项目中体现 Compose 列表能力如果你正准备把 Jetpack Compose 的项目经历写进简历列表是值得好好写的一块。你可以从这几个维度描述使用LazyColumn实现高性能列表通过稳定key保证列表项状态恢复。结合StateFlowViewModel构建列表数据层实现加载、刷新、分页。通过sealed class实现多类型 list item 混排支持 Banner、文章卡片、广告等不同业务块。这些描述比单纯写“使用 Jetpack Compose 开发 App”更能体现你的工程能力。面试官问到的时候你也能对应说出上面这些细节说明你确实实践过。8. 总结与下一步这一篇围绕“Jetpack Compose 声明式 UI 开发中的列表”做了完整的拆解。我们重点掌握了声明式 UI 与传统 RecyclerView Adapter 的本质差异。LazyColumn、LazyRow 的基础用法与物品、物品索引的区别。通过稳定 key 保持列表项状态。构造了一个文章列表 App并加入点击、收藏、横滑标签、动态分页等常见需求。分析了多类型列表项、加载更多、空状态和性能优化思路。整理了高频问题和排查方式。如果你把这一篇里的示例代码全部手打一遍再把“列表项类型混排”和“加载更多”改造成自己的项目Compose 列表这块基本就真正掌握了。接下来的学习路线建议继续深入学习 Compose 状态管理remember、mutableStateOf、StateFlow、Snapshot的区别和适用场景。把列表和 Navigation 结合实现点击 item 跳转详情页。学习用 Coil 或 Glide 的 Compose 扩展加载网络图片让列表更接近真实项目。在真实项目中尝试用 Jetpack Compose 重写一个小模块比如首页列表或消息列表边写边踩坑收获会非常大。如果你在练习中遇到了这里没提到的问题欢迎在评论区带上具体代码和报错信息我们一起排查。下一篇教程我计划把 Compose 中“状态管理”这块内容展开讲把 remember、rememberSaveable、StateFlow 这些概念串起来。如果感兴趣可以保持关注。
返回列表