天天看點

Android-Paging添加footer和header

文章目錄

    • 當問題出現
    • 解決方式(不應該提倡的一種奇淫巧計)
      • paging的建構流程是啥呢?
      • 正式改動

首先,如果你是對paging不熟悉或者沒用過的童鞋,我強烈推薦這個大佬的文章

反思|Android 清單分頁元件Paging的設計與實作:系統概述

他的系列文章寫的都很棒,強烈建議都看一下 ★★★★★★,要不然你在看到一些類的用法的時候會很懵逼。

當問題出現

Android-Paging添加footer和header
Android-Paging添加footer和header
動圖挂了,先看圖檔吧

相信大家都看出來問題出現在哪裡了,當我們以打開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的話,便出現了上文所提出的問題,當然上文的頁面不屬于我的職責範圍,那麼我就拿我負責的部分來看

Android-Paging添加footer和header

為了不跟其他的部分沖突,我選擇了重複造輪子,但是問題依舊存在,當我去掉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

Android-Paging添加footer和header

光看方法名就應該感覺到,**的這個類就沒想着讓你改裡面的東西,後面幾步也是同樣的道理,我們再也接觸不到正經的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的位置。

Android-Paging添加footer和header

有的眼尖的同學露出了笑容,感覺要翻車。因為你每次請求一部分資料,每次後面都添加了一個footer标志的資料類,會出現啥情況呢?

Android-Paging添加footer和header

But,咱們寫這個文章就是為了解決這個問題的,經過一些處理,最終沒有翻車,我們呈上代碼(adapter的代碼)

對了,關于footer為啥會占據全屏寬度,上面有代碼,下面也有(在onBindViewHolder裡),我把adapter裡面所有的代碼都貼出來了

/**
 * Time:2020/4/13 20:29
 * Author: han1254
 * Email: [email protected]
 * 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了。

Android-Paging添加footer和header
Android-Paging添加footer和header

那麼到現在,我們的需求基本實作了,期間我們沒有改變源碼(主要還是技術太菜了😰),沒有改動基本的paging的使用邏輯,隻是在獲得資料以及處理資料時做了一點小手腳,友善快捷,感覺用這種方式實作header也是可以的,思路也不太難想。

這就是我的一點小見解,還是那句話,大佬不要噴的太狠😂