Paging在RecyclerView中的應(yīng)用,有這一篇就夠了

image

前言

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

Android Architecture Components Part1:Room

Android Architecture Components Part2:LiveData

Android Architecture Components Part3:Lifecycle

Android Architecture Components Part4:ViewModel

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

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

但Paging的唯一局限性是,它需要與RecyclerView結(jié)合使用,同時也要使用專有的PagedListAdapter。這是因為,它會將數(shù)據(jù)統(tǒng)一封裝成一個PagedList對象,而adapter持有該對象,一切數(shù)據(jù)的更新與變動都是通過PagedList來觸發(fā)。

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

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

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

Database中的使用

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

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

DataSource.Factory

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

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

我們只需拿到實現(xiàn)DataSource.Factory抽象的實例即可。

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

LiveData

現(xiàn)在我們在ViewMode中調(diào)用上面的getAll方法獲取所有的文章信息,并且將返回的數(shù)據(jù)封裝成一個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擴展方法來構(gòu)建PagedList的LiveData數(shù)據(jù)。其中Config中的參數(shù)代表每頁請求的數(shù)據(jù)個數(shù)。

我們已經(jīng)拿到了LiveData數(shù)據(jù),接下來進入第三步

PagedListAdapter

前面已經(jīng)說了,我們要實現(xiàn)PagedListAdapter,并將第二步拿到的數(shù)據(jù)傳入給它。

PagedListAdapter與RecyclerView.Adapter的使用區(qū)別不大,只是對getItemCount與getItem進行了重寫,因為它使用到了DiffUtil,避免對數(shù)據(jù)的無用更新。

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也已經(jīng)構(gòu)建完成,最后一旦PagedList被觀察到,使用submitList傳入到adapter即可。

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

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

自定義DataSource

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

但是我們實際開發(fā)中數(shù)據(jù)絕大多數(shù)來自于網(wǎng)絡(luò),所以DataSource與DataSource.Factory的實現(xiàn)還是要我們自己來啃。

所幸的是,對于DataSource的實現(xiàn),Paging已經(jīng)幫我們提供了三個非常全面的實現(xiàn),分別是:

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

PositionalDataSource相信已經(jīng)有點印象了吧,Room中默認(rèn)幫我實現(xiàn)的就是通過PositionalDataSource來獲取數(shù)據(jù)庫中的數(shù)據(jù)的。

接下來我們通過使用最廣的PageKeyedDataSource來實現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)。

基于Databases的三步,我們這里將它的第一步拆分為兩步,所以我們只需四步就能實現(xiàn)Paging對網(wǎng)絡(luò)數(shù)據(jù)的處理。

  1. 基于PageKeyedDataSource實現(xiàn)網(wǎng)絡(luò)請求
  2. 實現(xiàn)DataSource.Factory
  3. 使用LiveData來觀察PagedList
  4. 使用PagedListAdapter來與數(shù)據(jù)進行綁定與更

PageKeyedDataSource

我們自定義的DataSource需要實現(xiàn)PageKeyedDataSource,實現(xiàn)了之后會有如下三個方法需要我們?nèi)崿F(xiàn)

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>) {
        // 初始化第一頁數(shù)據(jù)
    }
    
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        // 加載下一頁數(shù)據(jù)
    }

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

其中l(wèi)oadBefore暫時用不到,因為我這個實例是獲取新聞列表,所以只需要loadInitial與loadAfter即可。

至于這兩個方法的具體實現(xiàn),其實沒什么多說的,根據(jù)你的業(yè)務(wù)要求來即可,這里要說的是,數(shù)據(jù)獲取完畢之后要回調(diào)方法第二個參數(shù)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方法中,我們將獲取的數(shù)據(jù)填充到onResult方法中,同時傳入了之前的頁碼previousPageKey(初始化為第一頁)與之后的頁面nextPageKey,nextPageKey自然是作用于loadAfter方法。這樣我們就可以在loadAfter中的params參數(shù)中獲取到:

    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就基本上完成了,接下來要做的是,實現(xiàn)DataSource.Factory來生成我們自定義的DataSource

DataSource.Factory

之前我們就已經(jīng)提及到,DataSource.Factory只有一個abstract方法,我們只需實現(xiàn)它的create方法來創(chuàng)建自定義的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進行封裝,統(tǒng)一通過sendRequest抽象方法來獲取NewsListingModel的封裝結(jié)果實例。

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,它里面包含了數(shù)據(jù)列表、加載狀態(tài)、刷新狀態(tài)、重試與刷新請求。

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中就相對來就簡單許多了,它需要關(guān)注的就是對NewsListingModel中的數(shù)據(jù)進行分離成單個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的基本類似,主要也是需要實現(xiàn)DiffUtil.ItemCallback,剩下的就是正常的Adapter實現(xiàn),我這里就不再多說了,如果需要的話請閱讀源碼

最后的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()
    }
image

Paging封裝的還是非常好的,尤其是項目中對RecyclerView非常依賴的,還是效果不錯的。當(dāng)然它的優(yōu)點也是它的局限性,這一點也是沒辦法的事情。

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

項目地址

Android精華錄

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

Android精華錄

blog

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容