本文已受權「玉剛說」微信公衆號獨家發佈android
Paging
是Google
在2018年I/O大會上推出的適用於Android
原生開發的分頁庫,若是您還不是很瞭解這個 官方欽定 的分頁架構組件,歡迎參考筆者的這篇文章:git
Android官方架構組件Paging:分頁庫的設計美學github
筆者在實際項目中已經使用Paging
半年有餘,和市面上其它熱門的分頁庫相比,Paging
最大的亮點在於其 將列表分頁加載的邏輯做爲回調函數封裝入 DataSource
中,開發者在配置完成後,無需控制分頁的加載,列表會 自動加載 下一頁數據並展現。編程
本文將闡述:爲使用了Paging
的列表添加Header
和Footer
的整個過程、這個過程當中遇到的一些阻礙、以及本身是如何解決這些阻礙的——若是您想直接瀏覽最終的解決方案,請直接翻閱本文的 最終的解決方案 小節。微信
爲RecyclerView
列表添加Header
或Footer
並非一個很麻煩的事,最簡單粗暴的方式是將RecyclerView
和Header
共同放入同一個ScrollView
的子View
中,但它無異於對RecyclerView
自身的複用機制視而不見,所以這種解決方案並不是首選。架構
更適用的解決方式是經過實現 多類型列表(MultiType),除了列表自己的Item
類型以外,Header
或Footer
也被視做一種Item
,關於這種方式的實現網上已有不少文章講解,本文不贅述。app
在正式開始本文內容以前,咱們先來看看最終的實現效果,咱們爲一個Student
的分頁列表添加了一個Header
和Footer
:框架
實現這種效果,筆者最初的思路也是經過 多類型列表 實現Header
和Footer
,可是很快咱們就遇到了第一個問題,那就是 咱們並無直接持有數據源。ide
對於常規的多類型列表而言,咱們能夠輕易的持有List<ItemData>
,從數據的控制而言,我更傾向於用一個表明Header
或者Footer
的佔位符插入到數據列表的頂部或者底部,這樣對於RecyclerView
的渲染而言,它是這樣的:函數
正如我所標註的,List<ItemData>
中一個ItemData
對應了一個ItemView
——我認爲爲一個Header
或者Footer
單首創建對應一個Model
類型是徹底值得的,它極大加強了代碼的可讀性,並且對於複雜的Header
而言,表明狀態的Model
類也更容易讓開發者對其進行渲染。
這種實現方式簡單、易讀而不失優雅,可是在Paging
中,這種思路一開始就被堵死了。
咱們先看PagedListAdapter
類的聲明:
// T泛型表明數據源的類型,即本文中的 Student
public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
// ...
}
複製代碼
所以,咱們須要這樣實現:
// 這裏咱們只能指定Student類型
class SimpleAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) {
// ...
}
複製代碼
有同窗提出,咱們能夠將這裏的Student
指定爲某個接口(好比定義一個ItemData
接口),而後讓Student
和Header
對應的Model
都去實現這個接口,而後這樣:
class SimpleAdapter : PagedListAdapter<ItemData, RecyclerView.ViewHolder>(diffCallback) {
// ...
}
複製代碼
看起來確實可行,可是咱們忽略了一個問題,那就是本小節要闡述的:
咱們並無直接持有數據源。
回到初衷,咱們知道,Paging
最大的亮點在於 自動分頁加載,這是觀察者模式的體現,配置完成後,咱們並不關心 數據是如何被分頁、什麼時候被加載、如何被渲染 的,所以咱們也不須要直接持有List<Student>
(實際上也持有不了),更無從談起手動爲其添加HeaderItem
和FooterItem
了。
以本文爲例,實際上全部邏輯都交給了ViewModel
:
class CommonViewModel(app: Application) : AndroidViewModel(app) {
private val dao = StudentDb.get(app).studentDao()
fun getRefreshLiveData(): LiveData<PagedList<Student>> =
LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder()
.setPageSize(15) //配置分頁加載的數量
.setInitialLoadSizeHint(40) //初始化加載的數量
.build()).build()
}
複製代碼
能夠看到,咱們並未直接持有List<Student>
,所以list.add(headerItem)
這種 持有並修改數據源 的方案几乎不可行(較真而言,實際上是可行的,可是成本太高,本文不深刻討論)。
接下來我針對直接實現多類型列表進行嘗試,咱們先不討論如何實現Footer
,僅以Header
而言,咱們進行以下的實現:
class HeaderSimpleAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) {
// 1.根據position爲item分配類型
// 若是position = 1,視爲Header
// 若是position != 1,視爲普通的Student
override fun getItemViewType(position: Int): Int {
return when (position == 0) {
true -> ITEM_TYPE_HEADER
false -> super.getItemViewType(position)
}
}
// 2.根據不一樣的viewType生成對應ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_TYPE_HEADER -> HeaderViewHolder(parent)
else -> StudentViewHolder(parent)
}
}
// 3.根據holder類型,進行對應的渲染
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.renderHeader()
is StudentViewHolder -> holder.renderStudent(getStudentItem(position))
}
}
// 4.這裏咱們根據StudentItem的position,
// 獲取position-1位置的學生
private fun getStudentItem(position: Int): Student? {
return getItem(position - 1)
}
// 5.由於有Header,item數量要多一個
override fun getItemCount(): Int {
return super.getItemCount() + 1
}
// 省略其餘代碼...
// 省略ViewHolder代碼
}
複製代碼
代碼和註釋已經將個人我的思想展現的很清楚了,咱們固定一個Header
在多類型列表的最上方,這也致使咱們須要重寫getItemCount()
方法,而且在對Item
進行渲染的onBindViewHolder()
方法中,對Sutdent
的獲取進行額外的處理——由於多了一個Header,致使產生了數據源和列表的錯位差—— 第n個數據被獲取時,咱們應該將其渲染在列表的第n+1個位置上。
我簡單繪製了一張圖來描述這個過程,也許更加直觀易懂:
代碼寫完後,直覺告訴我彷佛沒有什麼問題,讓咱們來看看實際的運行效果:
Gif也許展現並不那麼清晰,簡單總結下,問題有兩個:
Header
更應該是一個靜態獨立的組件,但實際上它也是列表的一部分,所以白光一閃,除了Student
列表,Header
做爲Item
也進行了刷新,這與咱們的預期不符;致使這兩個問題的根本緣由仍然是Paging
計算列表的position
時出現的問題:
對於問題1,Paging
對於列表的刷新理解爲 全部Item的刷新,所以一樣做爲Item
的Header
也沒法避免被刷新;
問題2依然也是這個問題致使的,在Paging
獲取到第一頁數據時(假設第一頁數據只有10條),Paging
會命令更新position in 0..9
的Item
,而實際上由於Header
的關係,咱們是指望它可以更新第position in 1..10
的Item
,最終致使了刷新以及對新數據的展現出現了問題。
正如標題而言,我嘗試求助於Google
和Github
,最終找到了這個連接:
PagingWithNetworkSample - PagedList RecyclerView scroll bug
若是您簡單研究過PagedListAdapter
的源碼的話,您應該瞭解,PagedListAdapter
內部定義了一個AsyncPagedListDiffer
,用於對列表數據的加載和展現,PagedListAdapter
更像是一個空殼,全部分頁相關的邏輯實際都 委託 給了AsyncPagedListDiffer
:
public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
final AsyncPagedListDiffer<T> mDiffer;
public void submitList(@Nullable PagedList<T> pagedList) {
mDiffer.submitList(pagedList);
}
protected T getItem(int position) {
return mDiffer.getItem(position);
}
public int getItemCount() {
return mDiffer.getItemCount();
}
public PagedList<T> getCurrentList() {
return mDiffer.getCurrentList();
}
}
複製代碼
雖然Paging
中數據的獲取和展現咱們是沒法控制的,但咱們能夠嘗試 瞞過 PagedListAdapter
,即便Paging
獲得了position in 0..9
的List<Data>
,可是咱們讓PagedListAdapter
去更新position in 1..10
的item不就能夠了嘛?
所以在上方的Issue
連接中,onlymash 同窗提出了一個解決方案:
重寫PagedListAdapter
中被AsyncPagedListDiffer
代理的全部方法,而後實例化一個新的AsyncPagedListDiffer
,並讓這個新的differ代理這些方法。
篇幅所限,咱們只展現部分核心代碼:
class PostAdapter: PagedListAdapter<Any, RecyclerView.ViewHolder>() {
private val adapterCallback = AdapterListUpdateCallback(this)
// 當第n個數據被獲取,更新第n+1個position
private val listUpdateCallback = object : ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) {
adapterCallback.onChanged(position + 1, count, payload)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
adapterCallback.onMoved(fromPosition + 1, toPosition + 1)
}
override fun onInserted(position: Int, count: Int) {
adapterCallback.onInserted(position + 1, count)
}
override fun onRemoved(position: Int, count: Int) {
adapterCallback.onRemoved(position + 1, count)
}
}
// 新建一個differ
private val differ = AsyncPagedListDiffer<Any>(listUpdateCallback,
AsyncDifferConfig.Builder<Any>(POST_COMPARATOR).build())
// 將全部方法重寫,並委託給新的differ去處理
override fun getItem(position: Int): Any? {
return differ.getItem(position - 1)
}
// 將全部方法重寫,並委託給新的differ去處理
override fun submitList(pagedList: PagedList<Any>?) {
differ.submitList(pagedList)
}
// 將全部方法重寫,並委託給新的differ去處理
override fun getCurrentList(): PagedList<Any>? {
return differ.currentList
}
}
複製代碼
如今咱們成功實現了上文中咱們的思路,一圖勝千言:
上一小節的實現方案是徹底可行的,但我我的認爲美中不足的是,這種方案 對既有的Adapter
中代碼改動過大。
我新建了一個AdapterListUpdateCallback
、一個ListUpdateCallback
以及一個新的AsyncPagedListDiffer
,並重寫了太多的PagedListAdapter
的方法——我添加了數十行分頁相關的代碼,但這些代碼和正常的列表展現並無直接的關係。
固然,我能夠將這些邏輯都抽出來放在一個新的類裏面,但我仍是感受我 好像是模仿並重寫了一個新的PagedListAdapter
類同樣,那麼是否還有其它的思路呢?
最終我找到了這篇文章:
Android RecyclerView + Paging Library 添加頭部刷新會自動滾動的問題分析及解決
這篇文章中的做者經過細緻分析Paging
的源碼,得出了一個更簡單實現Header
的方案,有興趣的同窗能夠點進去查看,這裏簡單闡述其原理:
經過查看源碼,以添加分頁爲例,Paging
對拿到最新的數據後,對列表的更新實際是調用了RecyclerView.Adapter
的notifyItemRangeInserted()
方法,而咱們能夠經過重寫Adapter.registerAdapterDataObserver()
方法,對數據更新的邏輯進行調整:
// 1.新建一個 AdapterDataObserverProxy 類繼承 RecyclerView.AdapterDataObserver
class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver {
RecyclerView.AdapterDataObserver adapterDataObserver;
int headerCount;
public ArticleDataObserver(RecyclerView.AdapterDataObserver adapterDataObserver, int headerCount) {
this.adapterDataObserver = adapterDataObserver;
this.headerCount = headerCount;
}
@Override
public void onChanged() {
adapterDataObserver.onChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload);
}
// 當第n個數據被獲取,更新第n+1個position
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount);
}
}
// 2.對於Adapter而言,僅需重寫registerAdapterDataObserver()方法
// 而後用 AdapterDataObserverProxy 去作代理便可
class PostAdapter extends PagedListAdapter {
@Override
public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
super.registerAdapterDataObserver(new AdapterDataObserverProxy(observer, getHeaderCount()));
}
}
複製代碼
咱們將額外的邏輯抽了出來做爲一個新的類,思路和上一小節的十分類似,一樣咱們也獲得了預期的結果。
通過對源碼的追蹤,從性能上來說,這兩種實現方式並無什麼不一樣,惟一的區別就是,前者是針對PagedListAdapter
進行了重寫,將Item
更新的代碼交給了AsyncPagedListDiffer
;而這種方式中,AsyncPagedListDiffer
內部對Item
更新的邏輯,最終仍然是交給了RecyclerView.Adapter
的notifyItemRangeInserted()
方法去執行的——二者本質上並沒有區別。
雖然上文只闡述了Paging library
如何實現Header
,實際上對於Footer
而言也是同樣,由於Footer
也能夠被視爲另一種的Item
;同時,由於Footer
在列表底部,並不會影響position
的更新,所以它更簡單。
下面是完整的Adapter
示例:
class HeaderProxyAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) {
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> ITEM_TYPE_HEADER
itemCount - 1 -> ITEM_TYPE_FOOTER
else -> super.getItemViewType(position)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_TYPE_HEADER -> HeaderViewHolder(parent)
ITEM_TYPE_FOOTER -> FooterViewHolder(parent)
else -> StudentViewHolder(parent)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.bindsHeader()
is FooterViewHolder -> holder.bindsFooter()
is StudentViewHolder -> holder.bindTo(getStudentItem(position))
}
}
private fun getStudentItem(position: Int): Student? {
return getItem(position - 1)
}
override fun getItemCount(): Int {
return super.getItemCount() + 2
}
override fun registerAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) {
super.registerAdapterDataObserver(AdapterDataObserverProxy(observer, 1))
}
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<Student>() {
override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem == newItem
}
private const val ITEM_TYPE_HEADER = 99
private const val ITEM_TYPE_FOOTER = 100
}
}
複製代碼
若是你想查看運行完整的demo,這裏是本文sample的地址:
文末最終的方案是否有更多優化的空間呢?固然,在實際的項目中,對其進行簡單的封裝是更有意義的(好比Builder
模式、封裝一個Header
、Footer
甚至二者都有的裝飾器類、或者其它...)。
本文旨在描述Paging使用過程當中 遇到的問題 和 解決問題的過程,所以項目級別的封裝和實現細節不做爲本文的主要內容;關於Header
和Footer
在Paging
中的實現方式,若是您有更好的解決方案,期待與您的共同探討。
爭取打造 Android Jetpack 講解的最好的博客系列:
Android Jetpack 實戰篇:
Hello,我是卻把清梅嗅,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人我的博客或者Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?