原文地址:https://proandroiddev.com/paging-library-database-network-c8c3185cfe3f
在开发过程中有一个很常见的场景就是从数据库或者网络中加载数据时,有大量的实体不能一次被加载。
Only Database,Only Network
如果我们要从数据库或者网络中加载数据,使用分页的方式就不会那么复杂了,对于数据库来说,推荐使用Room with Paging Library,并且可以在网上找到大量的解决方案。对于网络加载来说,推荐使用Paging Library
或 Paginate
Database + Network
但是困难的是,如何将两者结合在一起:
- 首先我们不能等待网络的返回,应该使用数据库的缓存数据来渲染UI,这意味着,当我们请求第一页数据时不能只是向服务器发送请求并等待他返回。我们应该直接从数据库获取数据并且显示它,然后当网络有数据返回的时候,我们再去更新数据库中的数据。
- 第二,如果远程服务器上的某条数据被删除了,你将如何注意到这个。
Paging Library对于解决从数据库和网络加载数据有自己的解决方案,就是使用BoundaryCallback,你可以添加自己的BoundaryCallback,他会根据某些事件通知你。他提供了三个方法用来覆盖:
- onZeroItemsLoaded()从PageList的数据源(Room数据库)返回零条结果时调用。
- onItemAtFrontLoaded(T itemAtFront)加载PageList前面的数据项时被调用。
- onItemAtEndLoaded(T itemAtEnd)加载PageList后面的数据项时被调用。
要解决的问题
- 使用Paging Library来观察数据库
- 观察Recclerview以了解何时需要向服务器请求数据。
为了演示,此处使用Person的实体类:
@Entity(tableName = "persons")
data class Person(
@ColumnInfo(name = "id") @PrimaryKey val id: Long,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "update_time") val updateTime: Long
)
观察数据库
在dao中定义一个方法用来观察数据库并且返回DataSource.Factory<Int,Person>
@Dao
interface PersonDao {
@Query("SELECT * FROM persons ORDER BY update_time")
fun selectPaged(): DataSource.Factory<Int, Person>
}
现在在ViewModel中我们将使用工厂构建一个PageList。
class PersonsViewModel(private val dao: PersonDao) : ViewModel() {
val pagedListLiveData : LiveData<PagedList<Person>> by lazy {
val dataSourceFactory = personDao.selectPaged()
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.build()
LivePagedListBuilder(dataSourceFactory, config).build()
}
}
在我们的view中可以观察paged list
class PersonsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_persons)
viewModel.pagedListLiveData.observe(this, Observer{
pagedListAdapter.submitList(it)
})
}
}
观察RecyclerView
现在我们要做的就是观察list,并且根据list的位置去请求服务器为我们提供相应页面的数据。为了观察RecyclerView的位置我们可以使用一个简单的库Paginate。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_persons)
viewModel.pagedListLiveData.observe(this, Observer{
pagedListAdapter.submitList(it)
})
Paginate.with(recyclerView, this).build()
}
override fun onLoadMore() {
// send the request to get corresponding page
}
override fun isLoading(): Boolean = isLoading
override fun hasLoadedAllItems(): Boolean = hasLoadedAllItems
}
正如你所看到的那样,我们将recyclerview与Paginate绑定,有三个回调函数,isLoading返回网络状态,hasLoadedAllItems用来展示是否已经到最后一页,并且没有更多数据要从服务器加载,最重要的方法就是实现onLoadMore()方法。
在这一部分有三个事情需要做:
- 基于recyclerview的位置,请求服务器来返回正确的数据页。
- 使用从server获取到的新的数据来更新数据库,并且导致PageList更新并显示新的数据,不要忘了我们正在观察数据。
- 如果请求失败我们展示错误。
override fun onLoadMore() {
if (!isLoading) {
isLoading = true
viewModel.loadPersons(page++).observe(this, Observer { response ->
isLoading = false
if (response.isSuccessful()) {
hasLoadedAllItems = response.data.hasLoadedAllItems
} else {
showError(response.errorBody())
}
})
}
}
class PersonsViewModel(
private val dao: PersonDao,
private val networkHelper: NetworkHelper
) : ViewModel() {
fun loadPersons(page: Int): LiveData<Response<Pagination<Person>>> {
val response =
MutableLiveData<Response<Pagination<Person>>>()
networkHelper.loadPersons(page) {
dao.updatePersons(
it.data.persons,
page == 0,
it.hasLoadedAllItems)
response.postValue(it)
}
return response
}
}
正如看到的那样,从网络获取数据并更新到数据库中。
@Dao
interface PersonDao {
@Transaction
fun persistPaged(
persons: List<Person>,
isFirstPage: Boolean,
hasLoadedAllItems: Boolean) {
val minUpdateTime = if (isFirstPage) {
0
} else {
persons.first().updateTime
}
val maxUpdateTime = if (hasLoadedAllItems) {
Long.MAX_VALUE
} else {
persons.last().updateTime
}
deleteRange(minUpdateTime, maxUpdateTime)
insert(persons)
}
@Query("DELETE FROM persons WHERE
update_time BETWEEN
:minUpdateTime AND :maxUpdateTime")
fun deleteRange(minUpdateTime: Long, maxUpdateTime: Long)
@Insert(onConflict = REPLACE)
fun insert(persons: List<Person>)
}
首先,我们在dao中删除updateTime在服务器返回的列表中第一个和最后一个人之间的所有人员,然后将列表插入数据库。这个是为了确保在服务器上删除的任何人同时能够在本地数据库删除。