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 是一个用于在 Android 应用中实现分页加载数据的开源库。它是 Google 官方发布的最新版本的 Paging 库,相较于以往的版本,Paging3 在实现简单、功能强大和性能优化方面有了很大的改进。 首先,Android Paging3 提供了强大的数据加载和显示机制。它通过将数据分割成固定大小的数据块 (page),并在需要时按需加载和展示数据,实现了无限滚动加载的效果。相较于传统的 RecyclerView 分页加载,Paging3 更加灵活,可以自动处理数据的加载和卸载,无需手动实现判断是否到底部、加载更多等繁琐逻辑。同时,Paging3 还支持局部刷新、数据源无缝替换等操作,让数据的加载和显示更加简单和高效。 其次,Paging3 在性能方面进行了优化。它使用了异步数据加载和显示机制,可以在后台线程上进行数据加载,不会阻塞主线程。同时,Paging3 采用了数据预加载和缓存策略,可以将下一页的数据提前加载到内存中,从而提高用户体验和应用的响应速度。并且,Paging3 还支持数据的持久化存储,可以将加载的数据缓存到本地数据库或文件中,避免了重复加载数据的开销。 最后,Paging3 还提供了丰富的扩展功能和灵活的定制选项。开发者可以自定义数据加载策略、数据源类型、数据显示方式等,以满足不同的业务需求。同时,Paging3 还提供了相关的辅助类和工具方法,帮助开发者更加便捷地实现数据的分页加载和显示。 总结来说,Android Paging3 是一个功能强大、性能优越的分页加载库,可以帮助开发者轻松实现数据的分页加载和显示,提高应用的用户体验和性能表现。无论是处理大量数据的列表页,还是实现无限滚动加载的功能,Paging3 都是一个值得推荐的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值