Android 分页加载与状态管理

举报
yd_217846120 发表于 2026/09/20 08:44:11 2026/09/20
【摘要】 Android 分页加载与状态管理长列表通常采用分页加载减少首屏等待和内存占用。分页看似只是维护页码,实际还要处理刷新、追加、重复触发、空页、错误重试和数据去重。一个可靠的分页实现,应先明确服务端分页协议,再把刷新与追加建模为不同状态。 一、确认分页协议常见协议有页码分页和游标分页:页码分页通过页号和每页数量请求,适合相对稳定的数据集合。游标分页通过上一页返回的标记继续查询,更适合实时变化...

Android 分页加载与状态管理

长列表通常采用分页加载减少首屏等待和内存占用。分页看似只是维护页码,实际还要处理刷新、追加、重复触发、空页、错误重试和数据去重。一个可靠的分页实现,应先明确服务端分页协议,再把刷新与追加建模为不同状态。

一、确认分页协议

常见协议有页码分页和游标分页:

  • 页码分页通过页号和每页数量请求,适合相对稳定的数据集合。
  • 游标分页通过上一页返回的标记继续查询,更适合实时变化的数据流。

客户端不能自行把两种模式混合。是否还有下一页,应使用接口明确提供的字段;只有协议约定“返回数量小于页大小代表结束”时,才能用数量判断。

二、分开刷新状态与追加状态

data class PagingUiState<T>(
    val items: List<T> = emptyList(),
    val refreshing: Boolean = false,
    val appending: Boolean = false,
    val refreshError: String? = null,
    val appendError: String? = null,
    val nextCursor: String? = null,
    val endReached: Boolean = false
)

首次加载失败可能需要全屏错误页,追加失败则通常保留旧列表并在底部显示重试。把两者混为一个 loading 和一个 error,界面很难表达正确结果。

三、刷新要创建新一代数据

用户下拉刷新时,旧追加请求可能仍在执行。可以使用任务取消或请求代次,阻止旧结果追加到新列表。

private var generation = 0

fun refresh() {
    val currentGeneration = ++generation
    viewModelScope.launch {
        updateState { copy(refreshing = true, refreshError = null) }
        try {
            val page = repository.loadFirstPage()
            if (currentGeneration != generation) return@launch
            updateState {
                copy(
                    items = page.items,
                    refreshing = false,
                    nextCursor = page.nextCursor,
                    endReached = page.nextCursor == null
                )
            }
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: PagingException) {
            if (currentGeneration != generation) return@launch
            updateState {
                copy(refreshing = false, refreshError = exception.userMessage)
            }
        }
    }
}

如果项目已经使用官方分页组件,应复用其代次与取消机制,不需要再平行实现一套状态机。

四、防止重复追加

滚动监听可能连续触发多次。追加前至少检查:

  • 当前没有追加任务。
  • 当前没有刷新任务。
  • 还未到达末尾。
  • 下一页标记存在。
fun append() {
    val state = uiState.value
    if (state.refreshing || state.appending || state.endReached) return
    val cursor = state.nextCursor ?: return

    appendJob = viewModelScope.launch {
        loadNextPage(cursor)
    }
}

这些条件来自分页状态本身,不需要额外维护多个含义重叠的锁。

五、使用稳定标识去重

实时数据在翻页期间可能插入新记录,前后两页出现重复项。合并时可以按稳定业务标识去重,同时保留服务端顺序。

fun <T> mergeDistinct(
    current: List<T>,
    incoming: List<T>,
    idOf: (T) -> String
): List<T> {
    val seen = current.mapTo(HashSet(), idOf)
    return buildList {
        addAll(current)
        incoming.forEach { item ->
            if (seen.add(idOf(item))) add(item)
        }
    }
}

如果新页中的同标识对象应覆盖旧内容,就要改为按标识更新,而不是简单忽略。合并规则由数据语义决定。

六、正确处理空状态

第一页成功且无数据,才是页面空状态。追加页返回空列表并不代表整个列表为空,只可能表示到达末尾或服务端数据变化。

刷新期间如果已有内容,可以保留列表并展示局部刷新指示;是否清空旧数据,应根据产品对数据时效性的要求决定。

七、重试只重试失败阶段

首次加载失败时重试第一页;追加失败时使用原下一页标记重试追加。不要在追加错误后无条件刷新整表,否则用户会丢失当前位置,也会增加请求量。

重试过程中仍要防止重复点击,并保留取消语义。业务拒绝和参数错误不应无限自动重试。

八、控制列表内存

无限向后追加会让内存持续增长。超长数据流可以使用项目分页库的页面淘汰能力,或只保留业务需要的窗口。淘汰后恢复位置、返回上一页等体验需要一并设计。

界面模型应只包含展示必要字段,图片由统一加载组件按目标尺寸管理。

九、测试关键时序

分页测试至少包含:

  • 首次成功、空数据和失败。
  • 连续触底只发起一次追加。
  • 追加失败后旧列表仍然保留。
  • 刷新发生时旧追加结果不再生效。
  • 相邻页面重复数据按规则合并。
  • 到达末尾后不再请求。
  • 进程重建后筛选条件和分页入口正确恢复。

总结

分页加载的重点是状态与时序。明确页码或游标协议,分别管理刷新和追加,用代次或取消隔离旧请求,再按稳定标识合并数据。空状态、错误重试和末尾判断都遵循服务端契约,列表才能在快速滚动和实时变化中保持可靠。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。