一、完整版:
1. 依赖(稳定版)
gradle
arduino
implementation "androidx.compose.material3:material3:1.3.2"
// 推荐稳定(无实验标记)
implementation "androidx.compose.material3:material3:1.4.0" // ✅ 最佳
2.版本与组件对应关系
表格
| 版本区间 | 组件名称 | 状态 | 备注 |
|---|---|---|---|
| 1.0.x ~ 1.2.x | PullToRefreshContainer |
稳定 | 老版,需要自己套 Modifier.pullToRefresh |
| 1.3.0-alpha05 ~ 1.3.x | PullToRefreshBox |
@ExperimentalMaterial3Api | 首次出现,API 还在调整 |
| 1.4.0-alpha17 ~ 1.4.x+ | PullToRefreshBox |
稳定 | 去掉实验标记,官方推荐使用 |
一句话:
- 你用 1.2.x 及更早 → 只能用老的
PullToRefreshContainer - 你用 1.3.x → 有
PullToRefreshBox但要加实验注解 - 你用 1.4.0+ → 直接稳定使用
PullToRefreshBox
3、函数定义与作用
kotlin
less
@Composable
fun PullToRefreshBox(
isRefreshing: Boolean, // 1. 核心:是否正在刷新
onRefresh: () -> Unit, // 2. 核心:下拉触发的回调
modifier: Modifier = Modifier,
state: PullToRefreshState = rememberPullToRefreshState(),
indicator: @Composable BoxScope.() -> Unit = { 默认指示器 },
content: @Composable BoxScope.() -> Unit // 3. 核心:包裹的内容
)
一句话功能:
一个带下拉手势、自动显示刷新指示器、触发刷新回调 的盒子,内部必须放可滚动内容 (如
LazyColumn)。
4、你代码里的 3 个关键参数详解
1. modifier = Modifier.fillMaxSize()
- 普通修饰符,让
PullToRefreshBox占满整个屏幕。 - 必须加,否则内容可能显示不全 / 无法下拉。
2. isRefreshing = isRefreshing
-
类型 :
Boolean -
含义 :当前是否正在执行刷新任务Android Developers
true→ 显示转圈加载指示器 ,并锁定下拉手势false→ 隐藏指示器,允许用户下拉
-
作用 :双向控制
- 组件读它:决定显示 / 隐藏加载动画
- 你写它:异步任务开始 / 结束时手动更新
3. onRefresh = { isRefreshing = true }
-
类型 :
() -> Unit回调 -
触发时机 :用户下拉超过阈值(约 80dp)并松手时,系统自动调用
-
正确写法 :
kotlin
inionRefresh = { isRefreshing = true //...网络请求... isRefreshing = false }
4.例子
kotlin
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import kotlinx.coroutines.delay
@Composable
fun RefreshScreen() {
// 1. 定义刷新状态(必须用 remember 保存)
var isRefreshing by remember { mutableStateOf(false) }
// 协程作用域,用于执行异步任务
val coroutineScope = rememberCoroutineScope()
PullToRefreshBox(
modifier = Modifier.fillMaxSize(),
// 2. 绑定状态
isRefreshing = isRefreshing,
// 3. 正确的刷新回调
onRefresh = {
// 第一步:立即设为true,显示转圈
isRefreshing = true
// 第二步:执行你的异步任务(网络请求、数据库读取)
coroutineScope.launch {
// 模拟网络请求(替换成你的真实逻辑)
delay(1500)
// viewModel.refreshData()
// 第三步:任务结束,设回false,停止转圈
isRefreshing = false
}
}
) {
// 4. 内容区域:必须是可滚动组件
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(20) { index ->
Text(text = "列表项 $index", modifier = Modifier.fillMaxSize())
}
}
}
}
5.完整工作流程(必懂)
-
用户下拉 →
PullToRefreshBox检测手势 -
超过阈值松手 → 自动调用
onRefresh -
onRefresh内isRefreshing = true→ UI 显示加载圈- 执行网络 / IO 任务
-
数据加载完成
isRefreshing = false→ UI 隐藏加载圈,刷新完成
6.其他常用参数(进阶)
state: PullToRefreshState
- 内部管理下拉距离、进度
- 一般用默认的
rememberPullToRefreshState()即可,无需自定义
indicator(自定义刷新样式)
-
替换默认的 Material3 转圈箭头
-
示例:
kotlin
ini
indicator = {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.TopCenter),
color = Color.Blue
)
}
7. 总结(背会)
PullToRefreshBox= 官方下拉刷新壳isRefreshing= 控制转圈显示 / 隐藏的开关onRefresh= 下拉松手后执行的任务 (必须异步 + 结束置false){ ... }= 里面放LazyColumn等可滚动内容
二、完整代码:
1.完整代码:
scss
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
// 实体类
data class DataBean(val id: Int, val content: String)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PerfectRefreshLoadList() {
var dataList by remember { mutableStateOf(emptyList<DataBean>()) }
var currentPage by remember { mutableIntStateOf(1) }
val pageSize = 20
var isRefreshing by remember { mutableStateOf(false) }
var isLoadingMore by remember { mutableStateOf(false) }
var hasMoreData by remember { mutableStateOf(true) }
val listState = rememberLazyListState()
// ==========================================
// 1. 第一次进入页面:只加载第1页
// ==========================================
LaunchedEffect(Unit) {
isRefreshing = true
delay(800)
dataList = (1..pageSize).map { DataBean(it, "条目 $it") }
isRefreshing = false
}
// ==========================================
// 2. 下拉刷新
// ==========================================
LaunchedEffect(isRefreshing) {
if (isRefreshing) {
currentPage = 1
hasMoreData = true
delay(1000)
dataList = (1..pageSize).map { DataBean(it, "刷新条目 $it") }
isRefreshing = false
}
}
// ==========================================
// 3. 上拉加载更多(真正正确的写法)
// ==========================================
LaunchedEffect(listState) {//监听重组
snapshotFlow { listState.layoutInfo }//监听滑动
.distinctUntilChanged() // 关键:防止重复触发
.collect { layoutInfo ->
val totalItems = layoutInfo.totalItemsCount
val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
// 核心判断:只有 真正滑动到底部 才加载
val shouldLoad = !isRefreshing
&& !isLoadingMore
&& hasMoreData
&& totalItems > 0
&& lastVisibleIndex >= totalItems - 2
if (shouldLoad) {
isLoadingMore = true
currentPage++
// 模拟网络请求
delay(1200)
val start = dataList.size + 1
val newData = (start until start + pageSize).map {
DataBean(it, "加载更多 $it")
}
dataList = dataList + newData
// 最多加载5页
if (currentPage >= 5) hasMoreData = false
isLoadingMore = false
}
}
}
// ==========================================
// UI
// ==========================================
PullToRefreshBox(//是官方的指示器,也可以自定义的
modifier = Modifier.fillMaxSize(),
isRefreshing = isRefreshing,
onRefresh = { isRefreshing = true }
) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(dataList) { item ->
ListItem(headlineContent = { Text(item.content) })
}
if (isLoadingMore) {
item {
Box(
Modifier
.fillMaxWidth()
.padding(16.dp),
Alignment.Center
) {
CircularProgressIndicator()
}
}
}
if (!hasMoreData && dataList.isNotEmpty()) {
item {
Text(
"------ 已加载全部 ------",
Modifier.fillMaxWidth().padding(16.dp),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}
}
}
}
三、关键修复点(为什么这个版本不会乱触发)
1. 使用 snapshotFlow + distinctUntilChanged()
这是 Compose 官方唯一正确的列表滑动监听方式
- 第一次进入页面不会误触
- 只有滑动时才触发
- 不会重复执行
2. 严格判断状态
kotlin
arduino
!isRefreshing // 不在刷新
&& !isLoadingMore // 不在加载
&& hasMoreData // 还有数据
3. 只有滑动到底部才加载
不是第一次布局就执行。