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

24次阅读

共计 8433 个字符,预计需要花费 22 分钟才能阅读完成。

前言

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

一个基于 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

正文完
 0