美文网首页Android进阶之路Android补给站Android开发
Paging在RecyclerView中的应用,有这一篇就够了

Paging在RecyclerView中的应用,有这一篇就够了

作者: 午后一小憩 | 来源:发表于2019-07-31 09:54 被阅读3次
    image

    前言

    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)
    })
    
    image

    一个基于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()
        }
    
    image

    Paging封装的还是非常好的,尤其是项目中对RecyclerView非常依赖的,还是效果不错的。当然它的优点也是它的局限性,这一点也是没办法的事情。

    希望你通过这篇文章能够熟悉运用Paging,如果这篇文章对你有所帮助,你可以顺手关注一波,这是对我最大的鼓励!

    项目地址

    Android精华录

    该库的目的是结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点

    Android精华录

    blog

    相关文章

      网友评论

        本文标题:Paging在RecyclerView中的应用,有这一篇就够了

        本文链接:https://www.haomeiwen.com/subject/tsmarctx.html