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)
}
}
}
如果新页中的同标识对象应覆盖旧内容,就要改为按标识更新,而不是简单忽略。合并规则由数据语义决定。
六、正确处理空状态
第一页成功且无数据,才是页面空状态。追加页返回空列表并不代表整个列表为空,只可能表示到达末尾或服务端数据变化。
刷新期间如果已有内容,可以保留列表并展示局部刷新指示;是否清空旧数据,应根据产品对数据时效性的要求决定。
七、重试只重试失败阶段
首次加载失败时重试第一页;追加失败时使用原下一页标记重试追加。不要在追加错误后无条件刷新整表,否则用户会丢失当前位置,也会增加请求量。
重试过程中仍要防止重复点击,并保留取消语义。业务拒绝和参数错误不应无限自动重试。
八、控制列表内存
无限向后追加会让内存持续增长。超长数据流可以使用项目分页库的页面淘汰能力,或只保留业务需要的窗口。淘汰后恢复位置、返回上一页等体验需要一并设计。
界面模型应只包含展示必要字段,图片由统一加载组件按目标尺寸管理。
九、测试关键时序
分页测试至少包含:
- 首次成功、空数据和失败。
- 连续触底只发起一次追加。
- 追加失败后旧列表仍然保留。
- 刷新发生时旧追加结果不再生效。
- 相邻页面重复数据按规则合并。
- 到达末尾后不再请求。
- 进程重建后筛选条件和分页入口正确恢复。
总结
分页加载的重点是状态与时序。明确页码或游标协议,分别管理刷新和追加,用代次或取消隔离旧请求,再按稳定标识合并数据。空状态、错误重试和末尾判断都遵循服务端契约,列表才能在快速滚动和实时变化中保持可靠。
- 点赞
- 收藏
- 关注作者
评论(0)