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

Jetpeck paging3实践——无限加载网页列表数据

Android Jetpack Kotlin/Java pageing3的基础使用。

Paging3:在 Room DAO 中使用 PagingSource 作为返回类型时,“不确定如何将 Cursor 转换为此方法的返回类型”