Android Paging3 Footer踩坑优化
Posted bug樱樱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Paging3 Footer踩坑优化相关的知识,希望对你有一定的参考价值。
问题背景:
列表开发中一般都会有分页加载的需求,并且会定义一些边界状态(如下图),Google提供的Paging3分页加载组件可以完美高效的实现此功能,加载更多时的边界状态可以通过设置Header和Footer来处理。
其中加载中是好实现的,LoadStateAdapter本来的逻辑就是在loading和error状态显示item
//LoadStateAdapter源码中判断是否显示item
open fun displayLoadStateAsItem(loadState: LoadState): Boolean
return loadState is LoadState.Loading || loadState is LoadState.Error
而“没有更多了”显然是需要再加载完成后进行显示的,也就是在NotLoading状态下也要显示footer,那显然解决办法就是重写源码中的displayLoadStateAsItem()。
//自己定义的FooterAdapter中重写方法,使三种状态下Footer都可以显示出来
override fun displayLoadStateAsItem(loadState: LoadState): Boolean
return true
重写之后发现,确实在加载完之后会显示出“没有更多了”但是出现了一个新问题,进入列表后,会定位到第二页的位置,而不是在列表的顶部,如果加载时间比较久的话还会看到一个列表中只有一个“没有更多了”的item在头部,如下图:
解决方案:
这个问题有两种方案可以使用,可以酌情选择
方案一:
在refresh变动为NotLoading时,列表调用scrollToPosition(0),代码和效果如下
list.adapter = adapter.withLoadStateFooter(loadStateFooterAdapter)
//监听adapter的loadState
//在refresh变动为NotLoading时,列表调用scrollToPosition(0)
lifecycleScope.launchWhenCreated
adapter.loadStateFlow
// Only emit when REFRESH LoadState for RemoteMediator changes.
.distinctUntilChangedBy it.refresh
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
.filter it.refresh is LoadState.NotLoading
.collect list.scrollToPosition(0)
优点:容易理解,代码简单
缺点:只解决了列表的定位问题,还是会看到“没有更多了”的闪现
方案二:
在Footer中判断外部adapter的loadState来确认是否在NotLoading时显示item,代码和效果如下:
修改自己定义的Footer中代码
class LoadStateFooterAdapter(
val context: Context,
) : LoadStateAdapter()
//记录列表adapter的loadState
private var outLoadStates : CombinedLoadStates? = null
//记录自身是否被添加进RecycleView
var hasInserted = false
init
//注册监听,记录是否被添加
registerAdapterDataObserver(
object : RecyclerView.AdapterDataObserver()
override fun onItemRangeInserted(positionStart: Int, itemCount: Int)
super.onItemRangeInserted(positionStart, itemCount)
hasInserted = true
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int)
super.onItemRangeRemoved(positionStart, itemCount)
hasInserted = false
)
//更新外部LoadState
fun updateLoadState(loadState: CombinedLoadStates)
outLoadStates = loadState
//重写,增加判断逻辑
override fun displayLoadStateAsItem(loadState: LoadState): Boolean
//原有逻辑,loading和error状态下显示footer
val resultA = loadState is LoadState.Loading || loadState is LoadState.Error
//新增逻辑,refresh状态为NotLoading之后,NotLoading再显示footer
val resultB = (loadState is LoadState.NotLoading && outLoadStates?.refresh is LoadState.NotLoading)
val result = resultA || resultB
if (result && !hasInserted)
notifyItemInserted(0)
return result
override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState)
when (loadState)
is LoadState.Error ->
holder.binding.loadingView.text = "加载失败..."
is LoadState.Loading ->
holder.binding.loadingView.text = "加载中..."
is LoadState.NotLoading ->
holder.binding.loadingView.text = "没有更多了..."
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): FooterViewHolder
外部调用updateLoadState,更新LoadState
val loadStateFooterAdapter = LoadStateFooterAdapter(this)
list.adapter = adapter.withLoadStateFooter(loadStateFooterAdapter)
lifecycleScope.launchWhenCreated
adapter.loadStateFlow.collectLatest loadStates ->
//loadState更新近footerAdapter
loadStateFooterAdapter.updateLoadState(loadStates)
swipe_refresh.isRefreshing = loadStates.refresh is LoadState.Loading
优点:闪现和定位问题完美解决
缺点:暂未发现
方案解析:
问题分析:
要搞明白问题产生的原因需要看一下源码,以下为LoadStateAdapter相关源码
class LoadStateAdapter
//通过外部设置LoadState,判断如何显示或者隐藏Item
var loadState: LoadState = LoadState.NotLoading(endOfPaginationReached = false)
set(loadState)
if (field != loadState)
val oldItem = displayLoadStateAsItem(field)
val newItem = displayLoadStateAsItem(loadState)
if (oldItem && !newItem)
notifyItemRemoved(0)
else if (newItem && !oldItem)
notifyItemInserted(0)
else if (oldItem && newItem)
notifyItemChanged(0)
field = loadState
class PagingDataAdapter
//在设置footer的时候,添加一个监听,并将append的状态设置近footer里面
fun withLoadStateFooter(
footer: LoadStateAdapter<*>
): ConcatAdapter
addLoadStateListener loadStates ->
footer.loadState = loadStates.append
return ConcatAdapter(this, footer)
通过上述源码可知,给PagingDataAdapter的loadState状态发生改变的时候,会更新进LoadStateAdapter里触发显示逻辑,而这个LoadState是分多种的,也即如果refresh发生改变,那也会触发回调监听,而这时将append的默认值NotLoading设置进LoadStateAdapter,又因为将displayLoadStateAsItem的返回值改成true,触发了“没有更多了”的显示,后续refresh加载完成并显示列表,相当于是在头部添加数据,不滑动recycleView,则会表现为,定位在第一页的底部。
以上为,问题原因。
方案一解析:
方案一的解决方法就是,思路为:针对上述第一页会变成头部添加数据,那就在第一页refresh加载完成时滑动一下列表到头部呗,即解决了定位的问题,但也只解决了定位的问题。
方案二解析:
方案二的解决方法是针对整个问题做一个规避,思路为:既然refresh的变化导致了append的”没有更多了“的显示,那我修改displayLoadStateAsItem方法,在refresh的加载动作完成之前,footer还是保持只显示loading态和error态,在refresh完成加载变成NotLoaidng状态之后,再显示NotLoading状态的footer。所以需要外部adapter的LoadState更新时,将完整的LoadState传入footer中。
另外加了一段逻辑,即添加AdapterDataObserver来监听是否添加了item,这一段是针对LoadStateAdapter源码中判断老状态和新状态执行remove或者inster或者change做的防御处理,简单说就是,因为加入了refresh状态来判断displayLoadStateAsItem,所以可能会出现传入新状态后,判断老状态时的结果,和真实的结果不一致,也就会出现,上次通过一番判断后执行的remove操作,这次通过一番判断执行的change操作,就不会显示item了,比较绕,需要好好想一下。
其他方案:
本文推荐了两种解决方案,并且推荐第二种。但是针对问题原因应该还有很多解决方案,例如抛弃Paging3提供的LoadStateAdapter,直接自己定义一个,可能会更合理,不需要像方案二一样复杂,希望可以看到更多方案。
文末
我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。
需要的小伙伴直接点击文末小卡片免费领取哦,以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持,需要的自己领取)
Android学习PDF+架构视频+面试文档+源码笔记
部分资料一览:
- 330页PDF Android学习核心笔记(内含8大板块)
- Android学习的系统对应视频
- Android进阶的系统对应学习资料
- Android BAT大厂面试题(有解析)
领取地址:
以上是关于Android Paging3 Footer踩坑优化的主要内容,如果未能解决你的问题,请参考以下文章
Android Kotlin Paging3 Flow完整教程
在 Paging 3 库 Android Kotlin 中更新当前页面或更新数据
将 Paging 3 alpha 更新为稳定导致索引问题 Android
Android Jetpack Kotlin/Java pageing3的基础使用。
Paging3:在 Room DAO 中使用 PagingSource 作为返回类型时,“不确定如何将 Cursor 转换为此方法的返回类型”