Android Compose中使用官方自带的PullToRefreshBox

一、完整版:

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

    ini 复制代码
    onRefresh = { 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.完整工作流程(必懂)
  1. 用户下拉 → PullToRefreshBox 检测手势

  2. 超过阈值松手 → 自动调用 onRefresh

  3. onRefresh 内

    • isRefreshing = true → UI 显示加载圈
    • 执行网络 / IO 任务
  4. 数据加载完成

    • isRefreshing = false → UI 隐藏加载圈,刷新完成
6.其他常用参数(进阶)
  1. state: PullToRefreshState
  • 内部管理下拉距离、进度
  • 一般用默认的 rememberPullToRefreshState() 即可,无需自定义
  1. 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. 只有滑动到底部才加载

不是第一次布局就执行。

相关推荐
alexhilton1 天前
模块化Jetpack Compose架构
android·kotlin·android jetpack
hai_android1 天前
InputStage 责任链核心执行流程
android·android studio·android jetpack
传奇开心果编程2 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程3 天前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程3 天前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
天空之城--5 天前
Android一周动态:Android 18首次官宣、Compose Material3 1.4转正(5趋势+5资讯)
android·人工智能·flutter·架构·android jetpack
传奇开心果编程6 天前
【现代声明式UI学与练】第1课 从命令式UI到声明式UI
学习·flutter·ui·swiftui·react·android jetpack
传奇开心果编程9 天前
【用案例学Material 3 Expressive】第1课:从一封邮件开始,感受安卓设计新语言的表现力
android·学习·ui·kotlin·android jetpack
传奇开心果编程10 天前
【Jetpack Compose进阶学与练】第14课:系列收尾复习总结;Compose项目常见坑点汇总;学习路线与后续学习方向
android·学习·ui·kotlin·android jetpack
hai_android10 天前
子 View 被压小时,如何向父容器"告状"?——MEASURED_STATE_TOO_SMALL 向上传递机制
android·kotlin·android jetpack