原文地址:https://proandroiddev.com/paging-library-database-network-c8c3185cfe3f
在開發(fā)過程中有一個很常見的場景就是從數(shù)據(jù)庫或者網(wǎng)絡中加載數(shù)據(jù)時笔宿,有大量的實體不能一次被加載。
Only Database,Only Network
如果我們要從數(shù)據(jù)庫或者網(wǎng)絡中加載數(shù)據(jù),使用分頁的方式就不會那么復雜了惠昔,對于數(shù)據(jù)庫來說或衡,推薦使用Room with Paging Library,并且可以在網(wǎng)上找到大量的解決方案鳞绕。對于網(wǎng)絡加載來說龙填,推薦使用Paging Library
或 Paginate
Database + Network
但是困難的是螟左,如何將兩者結(jié)合在一起:
- 首先我們不能等待網(wǎng)絡的返回啡浊,應該使用數(shù)據(jù)庫的緩存數(shù)據(jù)來渲染UI觅够,這意味著胶背,當我們請求第一頁數(shù)據(jù)時不能只是向服務器發(fā)送請求并等待他返回。我們應該直接從數(shù)據(jù)庫獲取數(shù)據(jù)并且顯示它喘先,然后當網(wǎng)絡有數(shù)據(jù)返回的時候钳吟,我們再去更新數(shù)據(jù)庫中的數(shù)據(jù)。
- 第二窘拯,如果遠程服務器上的某條數(shù)據(jù)被刪除了红且,你將如何注意到這個。
Paging Library對于解決從數(shù)據(jù)庫和網(wǎng)絡加載數(shù)據(jù)有自己的解決方案涤姊,就是使用BoundaryCallback暇番,你可以添加自己的BoundaryCallback,他會根據(jù)某些事件通知你思喊。他提供了三個方法用來覆蓋:
- onZeroItemsLoaded()從PageList的數(shù)據(jù)源(Room數(shù)據(jù)庫)返回零條結(jié)果時調(diào)用壁酬。
- onItemAtFrontLoaded(T itemAtFront)加載PageList前面的數(shù)據(jù)項時被調(diào)用。
- onItemAtEndLoaded(T itemAtEnd)加載PageList后面的數(shù)據(jù)項時被調(diào)用恨课。
要解決的問題
- 使用Paging Library來觀察數(shù)據(jù)庫
- 觀察Recclerview以了解何時需要向服務器請求數(shù)據(jù)舆乔。
為了演示,此處使用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
)
觀察數(shù)據(jù)庫
在dao中定義一個方法用來觀察數(shù)據(jù)庫并且返回DataSource.Factory<Int,Person>
@Dao
interface PersonDao {
@Query("SELECT * FROM persons ORDER BY update_time")
fun selectPaged(): DataSource.Factory<Int, Person>
}
現(xiàn)在在ViewModel中我們將使用工廠構(gòu)建一個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
現(xiàn)在我們要做的就是觀察list希俩,并且根據(jù)list的位置去請求服務器為我們提供相應頁面的數(shù)據(jù)。為了觀察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綁定,有三個回調(diào)函數(shù)拖吼,isLoading返回網(wǎng)絡狀態(tài)鳞上,hasLoadedAllItems用來展示是否已經(jīng)到最后一頁,并且沒有更多數(shù)據(jù)要從服務器加載绿贞,最重要的方法就是實現(xiàn)onLoadMore()方法因块。
在這一部分有三個事情需要做:
- 基于recyclerview的位置,請求服務器來返回正確的數(shù)據(jù)頁籍铁。
- 使用從server獲取到的新的數(shù)據(jù)來更新數(shù)據(jù)庫涡上,并且導致PageList更新并顯示新的數(shù)據(jù),不要忘了我們正在觀察數(shù)據(jù)拒名。
- 如果請求失敗我們展示錯誤吩愧。
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
}
}
正如看到的那樣,從網(wǎng)絡獲取數(shù)據(jù)并更新到數(shù)據(jù)庫中增显。
@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在服務器返回的列表中第一個和最后一個人之間的所有人員,然后將列表插入數(shù)據(jù)庫。這個是為了確保在服務器上刪除的任何人同時能夠在本地數(shù)據(jù)庫刪除糖权。