前言
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 进行了解
- Database 中的使用
- 自定义 DataSource
Database 中的使用
Paging 在 Database 中的使用非常简单,它与 Room 结合将操作简单到了极致,我这里将其归纳于三步。
- 使用 DataSource.Factory 来获取 Room 中的数据
- 使用 LiveData 来观察 PagedList
- 使用 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 已经帮我们提供了三个非常全面的实现,分别是:
- PageKeyedDataSource: 通过当前页相关的 key 来获取数据,非常常见的是 key 作为请求的 page 的大小。
- ItemKeyedDataSource: 通过具体 item 数据作为 key,来获取下一页数据。例如聊天会话,请求下一页数据可能需要上一条数据的 id。
- PositionalDataSource: 通过在数据中的 position 作为 key,来获取下一页数据。这个典型的就是上面所说的在 Database 中的运用。
PositionalDataSource 相信已经有点印象了吧,Room 中默认帮我实现的就是通过 PositionalDataSource 来获取数据库中的数据的。
接下来我们通过使用最广的 PageKeyedDataSource 来实现网络数据。
基于 Databases 的三步,我们这里将它的第一步拆分为两步,所以我们只需四步就能实现 Paging 对网络数据的处理。
- 基于 PageKeyedDataSource 实现网络请求
- 实现 DataSource.Factory
- 使用 LiveData 来观察 PagedList
- 使用 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