文章目錄
-
- 當問題出現
- 解決方式(不應該提倡的一種奇淫巧計)
-
- paging的建構流程是啥呢?
- 正式改動
首先,如果你是對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: [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了。
那麼到現在,我們的需求基本實作了,期間我們沒有改變源碼(主要還是技術太菜了😰),沒有改動基本的paging的使用邏輯,隻是在獲得資料以及處理資料時做了一點小手腳,友善快捷,感覺用這種方式實作header也是可以的,思路也不太難想。
這就是我的一點小見解,還是那句話,大佬不要噴的太狠😂