Android-Paging添加footer和header


首先,如果你是对paging不熟悉或者没用过的童鞋,我强烈推荐这个大佬的文章
反思|Android 列表分页组件Paging的设计与实现:系统概述
他的系列文章写的都很棒,强烈建议都看一下 ★★★★★★,要不然你在看到一些类的用法的时候会很懵逼。

当问题出现

问题
动图挂了,先看图片吧

相信大家都看出来问题出现在哪里了,当我们以打开paging的页面,并没有显示第一行,而是直接显示了中间的某个部分,为了更好地跟实际项目接轨,这个我们是添加了footer的,用过paging的童鞋们应该都懂,PagedListAdapter是无法获得数据组的,对于出问题的这个界面,我们使用的添加footer的方式与传统的方式类似,(注意在onBindViewHolder里是如何设置瀑布流布局span的)

class CategoryPagingAdapter: PagedListAdapter<FirstClassificationBean.SearchListBean, RecyclerView.ViewHolder>(DIFF) {
   

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
   
       return when(viewType) {
   
           Int.MIN_VALUE -> CategoryFooterViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_category_footer, parent, false))

           else -> CategoryViewHolder.create(getItem(viewType), parent)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
   
    		if (position < itemCount - 1 ) {
   
               (holder as CategoryViewHolder).bindTo(getItem(position), position)
           } else {
   
           //这个是把footer设置为整个recyclerview的宽度,用过GridLayoutManager进行recyclerview混排
           //的童鞋一定对这个Span不陌生,但是对于瀑布流来说,想设置span为满屏宽度,基本上只有这一个方法
      
               val layoutParams = holder.itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams
               layoutParams.isFullSpan = true
           }
    }


    override fun getItemViewType(position: Int): Int {
   
        if (position == itemCount - 1) {
   
            return Int.MIN_VALUE
        }
        return position
    }

    override fun getItemCount(): Int {
   
        return super.getItemCount() + 1
    }

    companion object {
   

        val DIFF = object : DiffUtil.ItemCallback<FirstClassificationBean.SearchListBean>() {
   
            override fun areItemsTheSame(oldItem: FirstClassificationBean.SearchListBean, newItem: FirstClassificationBean.SearchListBean): Boolean = oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: FirstClassificationBean.SearchListBean, newItem: FirstClassificationBean.SearchListBean): Boolean = oldItem.id == newItem.id

        }
    }
}

如果去网上搜索的话,很容易就能找到
Android官方架构组件Paging-Ex:为分页列表添加Header和Footer
这篇文章,我们如果使用文中的方式添加footer的话,便出现了上文所提出的问题,当然上文的页面不属于我的职责范围,那么我就拿我负责的部分来看

为了不跟其他的部分冲突,我选择了重复造轮子,但是问题依旧存在,当我去掉footer的时候,一切就恢复正常了。
好像不符合上文作者说的😂

虽然上文只阐述了Paging library如何实现Header,实际上对于Footer而言也是一样,因为Footer也可以被视为另外一种的Item;同时,因为Footer在列表底部,并不会影响position的更新,因此它更简单。

现在我面临着学业压力以及开发周期的压力,是来不及看源码了,而且我觉得动不动就重写,修改源码本身不适合这种小毛病的解决,特别是在大项目中,修改源码,怕的是造成莫名的其他错误。
所以我想了个办法

解决方式(不应该提倡的一种奇淫巧计)

注意,本工程使用的MVVM架构
我是这么想的,我们不是不能接触到完整数据吗?但是我还是要改数据,让adapter只用来处理数据,我们让所有的操作都从我们设置进来的数据来推动,数据说是footer,我在adapter里判断,确实是footer(这里有点绕,或者不好理解,大家一会看到代码就懂了),就显示footer,adapter内部尽量不要搞太复杂的事情。我们看看对于使用paging时,哪些地方能够碰到数据。
回忆一下paging(纯网络)需要哪几个部分

  • DataSource
  • DatasourceFactory
  • PagedListAdapter

paging的构建流程是啥呢?

仅为个人粗鄙的理解,大佬慢点喷,大家尽量不要盲从,我这都是抽象的理解,不是基于大量的源码阅读的基础上的

①DataSource获得数据——>②callback发送——>③DataSourceFactory获得一个有数据的DataSource的快照——>④DataSourceFactory构建PagedList(然后在ViewModel中设置给一个LiveData)——>⑤View部分监测(专业一点叫观察,我喜欢叫成监测,感觉很酷)ViewModel里的那个设置了PagedList的LiveData——>⑥Adapter.submit(list);
这几个流程中,我们发现③根本没接触到数据,(下图的ids只是我用来远程请求数据的参数,retryExecutor是用来重请求的Executor)

//利用DataSourceFactory来构建一个DataSource的快照
class CategoryDataSourceFactory(
        private val retryExecutor: Executor,
        private val ids: String
) : DataSource.Factory<String, FirstClassificationBean.SearchListBean>() {
   
    val sourceLiveData = MutableLiveData<CategoryRemoteDataSource>()
    override fun create(): DataSource<String, FirstClassificationBean.SearchListBean> {
   
        val source = CategoryRemoteDataSource(retryExecutor, ids)
        sourceLiveData.postValue(source)
        return source
    }
}

④呢?这一步能接触到数据,但是接触到的是LiveData<PagedList<T>>

//利用DataSourceFactory构建LiveData<PagedList<T>>,一般是在ViewModel中使用
resultList = factory.toLiveData(pageSize = 20,
                fetchExecutor = executor)

想改动PagedList? Too young, too naive😀,我们来看看这个类的public method
在这里插入图片描述
光看方法名就应该感觉到,**的这个类就没想着让你改里面的东西,后面几步也是同样的道理,我们再也接触不到正经的List了,viewmodel里面获得的是pagedlsit,submit()里面传递的是pagedlist。③④⑤⑥直接歇菜,我们只能从①②两步,实施我们的邪恶计划。

正式改动

易知(高中数学老师成天用这句话来伤害我的内心,一做证明题就易知易知),①②一般在datasource里,那么我们可以看看DataSource里有啥

class CategoryRemoteDataSource(
        private val retryExecutor: Executor,
        private val ids: String
) : PageKeyedDataSource<String, FirstClassificationBean.SearchListBean>() {
   

    private var retry: (() -> Any)? = null
    private var page = 1
    val networkState = MutableLiveData<NetworkState>()
    val initialLoad = MutableLiveData<NetworkState>()
    fun retryAllFailed() {
   
        val prevRetry = retry
        retry = null
        prevRetry.let {
   
            retryExecutor.execute(
                    it?.invoke() as Runnable?
            )
        }
    }
    override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, FirstClassificationBean.SearchListBean>) {
   
        networkState.postValue(NetworkState.LOADING)
        initialLoad.postValue(NetworkState.LOADING)
        CategoriesRepository.getCategoriesDetail(object : BaseObserver<FirstClassificationBean>(null) {
   
            override fun onNext(t: FirstClassificationBean) {
   
                retry = null
                networkState.postValue(NetworkState.LOADED)
                initialLoad.postValue(NetworkState.LOADED)
                callback.onResult(getFadeData2().searchList, t.prevPage.toString(), t.nextPage.toString())
                page++
            }

            override fun onError(e: ApiException?) {
   
                retry = {
   
                    loadInitial(params, callback)
                }
                val error = NetworkState.error(e?.msg)
                networkState.postValue(error)
                initialLoad.postValue(error)
            }

        }, "1", params.requestedLoadSize.toString(), ids)
    }

    override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, FirstClassificationBean.SearchListBean>) {
   
        networkState.postValue(NetworkState.LOADING)
        //这个是网络请求,一般封装过RxJava+Retrofit的童鞋都知道这些基本操作
        CategoriesRepository.getCategoriesDetail(object : BaseObserver<FirstClassificationBean>(null) {
   
            override fun onNext(t: FirstClassificationBean) {
   
                networkState.postValue(NetworkState.LOADED)
                if (page < 5) {
   
                //在onResult之前,我们就可以对数据进行一番sao操作,
                  //t.searchList.add(XXXX),这个是正常的操作,对后端来的数据加上一个id为-1的结点
                  //代表这是Footer的位置
                  //但是我先用本地数据模拟替代了
                    callback.onResult(getFadeData2().searchList, t.nextPage.toString())
                    page++
                }
            }

            override fun onError(e: ApiException?) {
   
                retry = {
   
                    loadAfter(params, callback)
                }
                networkState.postValue(NetworkState.error(e?.msg))
            }

        }, page.toString(), params.requestedLoadSize.toString(), ids)
    }

    override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, FirstClassificationBean.SearchListBean>) {
   

    }


    fun getFadeData2() : FirstClassificationBean {
   
        var goods: MutableList<FirstClassificationBean.SearchListBean> = MutableList(10) {
    index ->
            var item = FirstClassificationBean.SearchListBean()
            item.wants = index
            item.name = "大果子"
            item.labelIds = "[\"2\",\"3\"]"
            item.price = 100.00f
            item.images = when (index % 3) {
   
                0, 3 -> "[\"http://q7opnl93o.bkt.clouddn.com/QQ%E5%9B%BE%E7%89%8720190414205835.png\"]"
                1 -> "[\"http://q7opnl93o.bkt.clouddn.com/3%EF%BC%9A4.png\"]"
                2 -> "[\"http://q7opnl93o.bkt.clouddn.com/4%EF%BC%9A3.png\"]"
                else -> "[\"http://q7opnl93o.bkt.clouddn.com/3%EF%BC%9A4.png\"]"
            }
            item.description = "这是一些测试的数据"
            item.userAvatar = "http://q7opnl93o.bkt.clouddn.com/QQ%E5%9B%BE%E7%89%8720190414205835.png"
            item.id = System.currentTimeMillis().toInt()
            item.userName = "龙猫"
            item
        }
        var data: FirstClassificationBean = FirstClassificationBean()
        val footerData = FirstClassificationBean.SearchListBean()
        footerData.id = -1
        goods.add(footerData)
        data.searchList = goods
        data.nextPage = 2
        data.prevPage = 0
        return data
    }

}

网络后端传递的数据有点少,我就自己构造了数据。每个数据的区分是靠id,而且正常情况下id不可能小于零,所以我在每次请求回来的分页数据的最后加上了一个id为-1的数据,Σ(っ °Д °;)っ,你为啥突然加个这种数据? 我们就是要用这个数据来占位,表明这个就是footer的位置。

有的眼尖的同学露出了笑容,感觉要翻车。因为你每次请求一部分数据,每次后面都添加了一个footer标志的数据类,会出现啥情况呢?

But,咱们写这个文章就是为了解决这个问题的,经过一些处理,最终没有翻车,我们呈上代码(adapter的代码)
对了,关于footer为啥会占据全屏宽度,上面有代码,下面也有(在onBindViewHolder里),我把adapter里面所有的代码都贴出来了

/** * Time:2020/4/13 20:29 * Author: han1254 * Email: 1254763408@qq.com * Function: */
class CategoryWithFooterAdapter: PagedListAdapter<FirstClassificationBean.SearchListBean, RecyclerView.ViewHolder>(DIFF) {
   

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
   
       return when(viewType) {
   
           FOOTER_TYPE -> CategoryFooterViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_category_footer, parent, false))
           NULL_TYPE -> CategoryFooterViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_paging_null_footer, parent, false))
           else -> CategoryViewHolder.create(getItem(viewType), parent)
        }
    }
    
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
   
        if (getItem(position)?.id == -1 ) {
   
            if (position == itemCount - 1) {
   
                val layoutParams = holder.itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams
                layoutParams.isFullSpan = true
            }
        } else {
   
            (holder as CategoryViewHolder).bindTo(getItem(position), position)
        }
    }


    override fun getItemViewType(position: Int): Int {
   
        if (getItem(position)?.id == -1 ) {
   
            return if (position == itemCount - 1) {
   
                FOOTER_TYPE
            } else {
   
                NULL_TYPE
            }
        }
        return position
    }



    companion object {
   

        val DIFF = object : DiffUtil.ItemCallback<FirstClassificationBean.SearchListBean>() {
   
            override fun areItemsTheSame(oldItem: FirstClassificationBean.SearchListBean, newItem: FirstClassificationBean.SearchListBean): Boolean = oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: FirstClassificationBean.SearchListBean, newItem: FirstClassificationBean.SearchListBean): Boolean = oldItem.id == newItem.id

        }

        const val FOOTER_TYPE = Int.MIN_VALUE
        const val NULL_TYPE = -1
    }
}

细心看代码的童鞋可能发现了,我比之前的adapter代码多了一个NULL_TYPE,NULL_TYPE对应的布局为

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="0dp" android:layout_height="0dp">

</androidx.constraintlayout.widget.ConstraintLayout>

Nothing,根本没有任何内容。我摊牌了,到这里我要解释一下我的实现思路——》

  • 在datasource里,为每一页数据插入header和footer数据(这些数据一定是要与普通的数据是同类或者是实现了共同接口的
  • 在adapter中,进行判断
override fun getItemViewType(position: Int): Int {
   
//如果id为-1,代表可能是footer类
       if (getItem(position)?.id == -1 ) {
   
       //如果是所有数据的最后一位,确定是footer
           return if (position == itemCount - 1) {
   
               FOOTER_TYPE
           } else {
   
           //否则,是空视图类,不占任何位置
               NULL_TYPE
           }
       }
       //普通数据,直接返回位置
       return position
   }

那么,我们根据不同的type来渲染不同的view

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
   
      return when(viewType) {
   
          FOOTER_TYPE -> CategoryFooterViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_category_footer, parent, false))
          //我承认我偷懒了,复用了footer的viewholder
          NULL_TYPE -> CategoryFooterViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_paging_null_footer, parent, false))
          else -> CategoryViewHolder.create(getItem(viewType), parent)
       }
   }

然后在onBindViewHolder进行类似getItemType()方法中的判断,如果id为-1但是不是最后一位,啥都不做,如果是最后一位且id为-1,则说明应该bind 真正的footer的viewholder了。

那么到现在,我们的需求基本实现了,期间我们没有改变源码(主要还是技术太菜了😰),没有改动基本的paging的使用逻辑,只是在获得数据以及处理数据时做了一点小手脚,方便快捷,感觉用这种方式实现header也是可以的,思路也不太难想。

这就是我的一点小见解,还是那句话,大佬不要喷的太狠😂

全部评论

相关推荐

不愿透露姓名的神秘牛友
11-21 19:05
点赞 评论 收藏
分享
勤奋努力的椰子这就开摆:美团骑手在美团工作没毛病
投递美团等公司10个岗位
点赞 评论 收藏
分享
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务