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. 只有滑动到底部才加载

不是第一次布局就执行。

相关推荐
阿巴斯甜1 天前
Hilt 的其它对象注入到普通类和普通类( @Inject constructor)注入到其它类(Activity或Fragment)
android jetpack
阿巴斯甜1 天前
Android中Hilt 的使用:
android jetpack
阿巴斯甜1 天前
Android 中InitializationProvider的使用:
android jetpack
我命由我123452 天前
Android Jetpack Compose - ModalNavigationDrawer、NavigationRail、PullToRefreshBox
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
我命由我123452 天前
Android buildSrc 模块问题:Gradle 的类 DefaultProject 被错误地尝试转换成 Apache Ant 的 Project 类
android·java·java-ee·kotlin·android jetpack·android-studio·android runtime
阿巴斯甜3 天前
Compose中 Saver的使用:
android jetpack
我命由我123453 天前
Android Jetpack Compose - SearchBar(搜索栏)、Tab(标签页)、时间选择器、TooltipBox(工具提示)
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
sp423 天前
NativeScript 的 Jetpack Compose 入门指南
android·android jetpack
我命由我123453 天前
Android Jetpack Compose - 组件分类:布局组件、交互组件、文本组件
android·java·java-ee·kotlin·android studio·android jetpack·android-studio