自接触 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 BOM(Bill of Materials)统一管理 Compose 各组件版本,而不是手动一个个写版本号。
2.2 添加 Compose 依赖
假设你是在已有 Android 项目里接入 Compose,核心依赖大致如下。这里以 Kotlin DSL(build.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 DSL(build.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 与 LazyRow
3.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是一个扩展函数,它接收一个List<T>,并为每个元素调用一次后面的渲染 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(): List<Article> { 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(setOf<Int>()) } 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: List<String>) { 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显示 CircularProgressIndicator,EmptyView显示一张占位图和“暂无数据”文案。这样结构清晰,也方便测试。
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。
坑 2:Compose 列表项内部状态丢失
比如 item 内有一个输入框,滑出屏幕再滑回来,输入内容不见了。原因通常是 item 被回收后组合状态没有保存。
解决方式:
- 使用
rememberSaveable保存状态。 - 把输入内容提升到 ViewModel 或父级状态中,而不是只存在 item 内部。
- 确保 key 稳定,使 Compose 能保存 item 的状态。
坑 3:Compose 编译器版本不匹配导致构建失败
这个在环境准备那节提到过。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 = MutableStateFlow<List<Article>>(emptyList()) val articles: StateFlow<List<Article>> = 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 中,不可变数据是状态管理的核心原则。线程切换、重组发生时,不可变数据可以避免很多“改了一半”的中间态问题。
具体到列表:
- 对外暴露的列表尽量使用
List<T>,而不是MutableList<T>。 - 数据更新时创建新列表,比如
list + newItem或者list.filter { ... }。 - 不要在 Composable 内部直接修改数据源。
这不仅是 Compose 的规定,也是函数式编程思路的体现:数据流清晰、可预测、更容易 Debug。
7.4 如何在简历项目中体现 Compose 列表能力
如果你正准备把 Jetpack Compose 的项目经历写进简历,列表是值得好好写的一块。你可以从这几个维度描述:
- 使用
LazyColumn实现高性能列表,通过稳定key保证列表项状态恢复。 - 结合
StateFlow+ViewModel构建列表数据层,实现加载、刷新、分页。 - 通过
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 这些概念串起来。如果感兴趣,可以保持关注。