问题背景:
列表开发中一般都会有分页加载的需求,并且会定义一些边界状态(如下图),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,直接自己定义一个,可能会更合理,不需要像方案二一样复杂,希望可以看到更多方案。