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,直接自己定义一个,可能会更合理,不需要像方案二一样复杂,希望可以看到更多方案。

Supongo que te gusta

Origin juejin.im/post/7073368998313721892
Recomendado
Clasificación