Paging在RecyclerView中的應用,有這一篇就夠了

前言

AAC是非常不錯的一套框架組件,如果你還未進行了解,推薦你閱讀我之前的系列文章:

Android Architecture Components Part1:Room

Android Architecture Components Part2:LiveData

Android Architecture Components Part3:Lifecycle

Android Architecture Components Part4:ViewModel

經過一年的發展,AAC又推出了一系列新的組件,幫助開發者更快的進行項目框架的構建與開發。這次主要涉及的是對Paging運用的全面介紹,相信你閱讀了這篇文章之後將對Paging的運用瞭如指掌。

Paging專注於有大量數據請求的列表處理,讓開發者無需關心數據的分頁邏輯,將數據的獲取邏輯完全與ui隔離,降低項目的耦合。

但Paging的唯一侷限性是,它需要與RecyclerView結合使用,同時也要使用專有的PagedListAdapter。這是因爲,它會將數據統一封裝成一個PagedList對象,而adapter持有該對象,一切數據的更新與變動都是通過PagedList來觸發。

這樣的好處是,我們可以結合LiveData或者RxJava來對PagedList對象的創建進行觀察,一旦PagedList已經創建,只需將其傳入給adapter即可,剩下的數據操更新操作將由adapter自動完成。相比於正常的RecyclerView開發,簡單了許多。

下面我們通過兩個具體實例來對Paging進行了解

  1. Database中的使用
  2. 自定義DataSource

Database中的使用

Paging在Database中的使用非常簡單,它與Room結合將操作簡單到了極致,我這裏將其歸納於三步。

  1. 使用DataSource.Factory來獲取Room中的數據
  2. 使用LiveData來觀察PagedList
  3. 使用PagedListAdapter來與數據進行綁定與更新

DataSource.Factory

首先第一步我們需要使用DataSource.Factory抽象類來獲取Room中的數據,它內部只要一個create抽象方法,這裏我們無需實現,Room會自動幫我們創建PositionalDataSource實例,它將會實現create方法。所以我們要做的事情非常簡單,如下:

@Dao
interface ArticleDao {
 
    // PositionalDataSource
    @Query("SELECT * FROM article")
    fun getAll(): DataSource.Factory<Int, ArticleModel>
}

我們只需拿到實現DataSource.Factory抽象的實例即可。

第一步就這麼簡單,接下來看第二步

LiveData

現在我們在ViewMode中調用上面的getAll方法獲取所有的文章信息,並且將返回的數據封裝成一個LiveData,具體如下:

class PagingViewModel(app: Application) : AndroidViewModel(app) {
    private val dao: ArticleDao by lazy { AppDatabase.getInstance(app).articleDao() }
 
    val articleList = dao.getAll()
            .toLiveData(Config(
                    pageSize = 5
            ))
}

通過DataSource.Factory的toLiveData擴展方法來構建PagedList的LiveData數據。其中Config中的參數代表每頁請求的數據個數。

我們已經拿到了LiveData數據,接下來進入第三步

PagedListAdapter

前面已經說了,我們要實現PagedListAdapter,並將第二步拿到的數據傳入給它。

PagedListAdapter與RecyclerView.Adapter的使用區別不大,只是對getItemCount與getItem進行了重寫,因爲它使用到了DiffUtil,避免對數據的無用更新。

class PagingAdapter : PagedListAdapter<ArticleModel, PagingVH>(diffCallbacks) {
 
    companion object {
        private val diffCallbacks = object : DiffUtil.ItemCallback<ArticleModel>() {

            override fun areItemsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem.id == newItem.id
 
            override fun areContentsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem == newItem

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingVH = PagingVH(R.layout.item_paging_article_layout, parent)
 
    override fun onBindViewHolder(holder: PagingVH, position: Int) = holder.bind(getItem(position))
}

這樣adapter也已經構建完成,最後一旦PagedList被觀察到,使用submitList傳入到adapter即可。

viewModel.articleList.observe(this, Observer {
    adapter.submitList(it)
})

clipboard.png

一個基於Paging的Database列表已經完成,是不是非常簡單呢?如果需要完整代碼可以查看Github

自定義DataSource

上面是通過Room來獲取數據,但我們需要知道的是,Room之所以簡單是因爲它會幫我們自己實現許多數據庫相關的邏輯代碼,讓我們只需關注與自己業務相關的邏輯即可。而這其中與Paging相關的是對DataSource與DataSource.Factory的具體實現。

但是我們實際開發中數據絕大多數來自於網絡,所以DataSource與DataSource.Factory的實現還是要我們自己來啃。

所幸的是,對於DataSource的實現,Paging已經幫我們提供了三個非常全面的實現,分別是:

  1. PageKeyedDataSource: 通過當前頁相關的key來獲取數據,非常常見的是key作爲請求的page的大小。
  2. ItemKeyedDataSource: 通過具體item數據作爲key,來獲取下一頁數據。例如聊天會話,請求下一頁數據可能需要上一條數據的id。
  3. PositionalDataSource: 通過在數據中的position作爲key,來獲取下一頁數據。這個典型的就是上面所說的在Database中的運用。

PositionalDataSource相信已經有點印象了吧,Room中默認幫我實現的就是通過PositionalDataSource來獲取數據庫中的數據的。

接下來我們通過使用最廣的PageKeyedDataSource來實現網絡數據。

基於Databases的三步,我們這裏將它的第一步拆分爲兩步,所以我們只需四步就能實現Paging對網絡數據的處理。

  1. 基於PageKeyedDataSource實現網絡請求
  2. 實現DataSource.Factory
  3. 使用LiveData來觀察PagedList
  4. 使用PagedListAdapter來與數據進行綁定與更

PageKeyedDataSource

我們自定義的DataSource需要實現PageKeyedDataSource,實現了之後會有如下三個方法需要我們去實現

class NewsDataSource(private val newsApi: NewsApi,
                     private val domains: String,
                     private val retryExecutor: Executor) : PageKeyedDataSource<Int, ArticleModel>() {
 
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, ArticleModel>) {
        // 初始化第一頁數據
    }
    
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        // 加載下一頁數據
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        // 加載前一頁數據
    }
}

其中loadBefore暫時用不到,因爲我這個實例是獲取新聞列表,所以只需要loadInitial與loadAfter即可。

至於這兩個方法的具體實現,其實沒什麼多說的,根據你的業務要求來即可,這裏要說的是,數據獲取完畢之後要回調方法第二個參數callback的onResult方法。例如loadInitial:

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, ArticleModel>) {
        initStatus.postValue(Loading(""))
        CompositeDisposable().add(getEverything(domains, 1, ArticleListModel::class.java)
                .subscribeWith(object : DisposableObserver<ArticleListModel>() {
                    override fun onComplete() {
                    }
 
                    override fun onError(e: Throwable) {
                        retry = {
                            loadInitial(params, callback)
                        }
                        initStatus.postValue(Error(e.localizedMessage))
                    }

                    override fun onNext(t: ArticleListModel) {
                        initStatus.postValue(Success(200))
                        callback.onResult(t.articles, 1, 2)
                    }
                }))
    }

在onNext方法中,我們將獲取的數據填充到onResult方法中,同時傳入了之前的頁碼previousPageKey(初始化爲第一頁)與之後的頁面nextPageKey,nextPageKey自然是作用於loadAfter方法。這樣我們就可以在loadAfter中的params參數中獲取到:

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        loadStatus.postValue(Loading(""))
        CompositeDisposable().add(getEverything(domains, params.key, ArticleListModel::class.java)
                .subscribeWith(object : DisposableObserver<ArticleListModel>() {
                    override fun onComplete() {
                    }
 
                    override fun onError(e: Throwable) {
                        retry = {
                            loadAfter(params, callback)
                        }
                        loadStatus.postValue(Error(e.localizedMessage))
                    }
 
                    override fun onNext(t: ArticleListModel) {
                        loadStatus.postValue(Success(200))
                        callback.onResult(t.articles, params.key + 1)
                    }
                }))
    }

這樣DataSource就基本上完成了,接下來要做的是,實現DataSource.Factory來生成我們自定義的DataSource

DataSource.Factory

之前我們就已經提及到,DataSource.Factory只有一個abstract方法,我們只需實現它的create方法來創建自定義的DataSource即可:

class NewsDataSourceFactory(private val newsApi: NewsApi,
                            private val domains: String,
                            private val executor: Executor) : DataSource.Factory<Int, ArticleModel>() {
 
    val dataSourceLiveData = MutableLiveData<NewsDataSource>()
 
    override fun create(): DataSource<Int, ArticleModel> {
        val dataSource = NewsDataSource(newsApi, domains, executor)
        dataSourceLiveData.postValue(dataSource)
        return dataSource
    }
}

嗯,代碼就是這麼簡單,這一步也就完成了,接下來要做的是將pagedList進行LiveData封裝。

Repository & ViewModel

這裏與Database不同的是,並沒有直接在ViewModel中通過DataSource.Factory來獲取pagedList,而是進一步使用Repository進行封裝,統一通過sendRequest抽象方法來獲取NewsListingModel的封裝結果實例。

data class NewsListingModel(val pagedList: LiveData<PagedList<ArticleModel>>,
                            val loadStatus: LiveData<LoadStatus>,
                            val refreshStatus: LiveData<LoadStatus>,
                            val retry: () -> Unit,
                            val refresh: () -> Unit)
 
sealed class LoadStatus : BaseModel()
data class Success(val status: Int) : LoadStatus()
data class NoMore(val content: String) : LoadStatus()
data class Loading(val content: String) : LoadStatus()
data class Error(val message: String) : LoadStatus()

所以Repository中的sendRequest返回的將是NewsListingModel,它裏面包含了數據列表、加載狀態、刷新狀態、重試與刷新請求。

class NewsRepository(private val newsApi: NewsApi,
                     private val domains: String,
                     private val executor: Executor) : BaseRepository<NewsListingModel> {
 
    override fun sendRequest(pageSize: Int): NewsListingModel {
        val newsDataSourceFactory = NewsDataSourceFactory(newsApi, domains, executor)
        val newsPagingList = newsDataSourceFactory.toLiveData(
                pageSize = pageSize,
                fetchExecutor = executor
        )
        val loadStatus = Transformations.switchMap(newsDataSourceFactory.dataSourceLiveData) {
            it.loadStatus
        }
        val initStatus = Transformations.switchMap(newsDataSourceFactory.dataSourceLiveData) {
            it.initStatus
        }
        return NewsListingModel(
                pagedList = newsPagingList,
                loadStatus = loadStatus,
                refreshStatus = initStatus,
                retry = {
                    newsDataSourceFactory.dataSourceLiveData.value?.retryAll()
                },
                refresh = {
                    newsDataSourceFactory.dataSourceLiveData.value?.invalidate()
                }
        )
    }

}

接下來ViewModel中就相對來就簡單許多了,它需要關注的就是對NewsListingModel中的數據進行分離成單個LiveData對象即可,由於本身其成員就是LiveDate對象,所以分離也是非常簡單。分離是爲了以便在Activity進行observe觀察。

class NewsVM(app: Application, private val newsRepository: BaseRepository<NewsListingModel>) : AndroidViewModel(app) {

    private val newsListing = MutableLiveData<NewsListingModel>()
 
    val adapter = NewsAdapter {
        retry()
    }
 
    val newsLoadStatus = Transformations.switchMap(newsListing) {
        it.loadStatus
    }
 
    val refreshLoadStatus = Transformations.switchMap(newsListing) {
        it.refreshStatus
    }
 
    val articleList = Transformations.switchMap(newsListing) {
        it.pagedList
    }
 
    fun getData() {
        newsListing.value = newsRepository.sendRequest(20)
    }
 
    private fun retry() {
        newsListing.value?.retry?.invoke()
    }
 
    fun refresh() {
        newsListing.value?.refresh?.invoke()
    }
}

PagedListAdapter & Activity

Adapter部分與Database的基本類似,主要也是需要實現DiffUtil.ItemCallback,剩下的就是正常的Adapter實現,我這裏就不再多說了,如果需要的話請閱讀源碼

最後的observe代碼

    private fun addObserve() {
        newsVM.articleList.observe(this, Observer {
            newsVM.adapter.submitList(it)
        })
        newsVM.newsLoadStatus.observe(this, Observer {
            newsVM.adapter.updateLoadStatus(it)
        })
        newsVM.refreshLoadStatus.observe(this, Observer {
            refresh_layout.isRefreshing = it is Loading
        })
        refresh_layout.setOnRefreshListener {
            newsVM.refresh()
        }
        newsVM.getData()
    }

clipboard.png

Paging封裝的還是非常好的,尤其是項目中對RecyclerView非常依賴的,還是效果不錯的。當然它的優點也是它的侷限性,這一點也是沒辦法的事情。

希望你通過這篇文章能夠熟悉運用Paging,如果這篇文章對你有所幫助,你可以順手關注一波,這是對我最大的鼓勵!

項目地址

Android精華錄

該庫的目的是結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點

Android精華錄

blog

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章