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

前言

AAC是很是不錯的一套框架組件,若是你還未進行了解,推薦你閱讀我以前的系列文章:java

Android Architecture Components Part1:Roomandroid

Android Architecture Components Part2:LiveDatagit

Android Architecture Components Part3:Lifecyclegithub

Android Architecture Components Part4:ViewModel數據庫

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

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

但Paging的惟一侷限性是,它須要與RecyclerView結合使用,同時也要使用專有的PagedListAdapter。這是由於,它會將數據統一封裝成一個PagedList對象,而adapter持有該對象,一切數據的更新與變更都是經過PagedList來觸發。網絡

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

下面咱們經過兩個具體實例來對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)
})
複製代碼

一個基於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()
    }
複製代碼

Paging封裝的仍是很是好的,尤爲是項目中對RecyclerView很是依賴的,仍是效果不錯的。固然它的優勢也是它的侷限性,這一點也是沒辦法的事情。

但願你經過這篇文章可以熟悉運用Paging,若是這篇文章對你有所幫助,你能夠順手關注一波,這是對我最大的鼓勵!

項目地址

Android精華錄

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

Android精華錄

blog

相關文章
相關標籤/搜索