我正在处理我的最后一个消息应用程序项目。我们在项目中做的最重要和最常见的事情之一是逐渐从网络或数据库中加载数据,这可能是因为无法一次加载大量实体。
如果您不熟悉分页库或实时数据概念,请先花时间研究它们,因为我不打算在这里讨论它们。您可以使用很多资源来学习它们。
我的解决方案包括两个主要部分!
- 使用分页库观察数据库。
- 观察 RecyclerView 以了解何时向服务器请求数据页。
为了演示,我们将使用一个表示 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
)
1.观察数据库
让我们从第一个更简单的开始:
为了观察数据库,我们将在 dao 中定义一个返回 DataSource.Factory
的方法
@Dao
interface PersonDao {
@Query("SELECT * FROM persons ORDER BY update_time DESC")
fun selectPaged(): DataSource.Factory<Int, Person>
}
现在在我们的 ViewModel 中,我们将从我们的工厂构建一个 PagedList
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()
}
}
从我们的角度来看,我们可以观察到分页列表
class PersonsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_persons)
viewModel.pagedListLiveData.observe(this, Observer{
pagedListAdapter.submitList(it)
})
}
}
好的,现在这基本上是我们第一部分应该做的。请注意,我们使用的是 PagedListAdapter。此外,我们可以对 PagedList.Config 对象进行更多自定义,但为简单起见,我们将其省略。再次请注意,我们没有在 LivePagedListBuilder 上使用 BoundaryCallback。
2。观察 RecyclerView
这里基本上我们应该做的是观察列表,根据我们现在在列表中的位置,请求服务器为我们提供相应的数据页面。为了观察 RecyclerView 的位置,我们将使用一个名为 Paginate 的简单库。
class PersonsActivity : AppCompatActivity(), Paginate.Callbacks {
private var page = 0
private var isLoading = false
private var hasLoadedAllItems = false
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
}
如您所见,我们将 Paginate 与回收器视图绑定,现在我们有三个回调。 isLoading() 应该返回网络的状态。 hasLoadedAllItems() 显示我们是否已经到达最后一页并且没有更多数据可以从服务器加载。我们所做的大部分工作是实现最后一个方法 onLoadMore()。
在这个阶段我们应该做三件事:
- 根据 recyclerView 的位置,我们要求服务器向我们展示正确的数据页面。
- 我们使用来自服务器的新数据更新数据库,从而更新 PagedList 并显示新数据。别忘了我们正在观察数据库!
- 如果请求失败,我们会显示错误。
通过这些简单的步骤,我们解决了两个问题。
首先,尽管 BoundaryCallbak 没有回调来获取已获取的数据,但我们正在按需请求每个页面,以便我们可以注意到更新的实体并更新我们自己的本地数据库。
其次,我们可以很容易地显示网络的状态,也可以显示可能的网络故障。
听起来不错吧?好吧,我们还没有解决一个特定的问题。这就是如果一个实体从远程服务器中删除的情况。我们怎么会注意到这一点!这就是数据排序的用武之地。通过一个非常古老的数据排序技巧,我们可以注意到我们之间的差距。例如,如果服务器返回的 JSON 页面如下所示,我们现在可以根据他们的 update_time 对我们的人员进行排序:
{
"persons": [{
"id": 1,
"name": "Reza",
"update_time": 1535533985000
}, {
"id": 2,
"name": "Nick",
"update_time": 1535533985111
}, {
"id": 3,
"name": "Bob",
"update_time": 1535533985222
}, {
"id": 4,
"name": "Jafar",
"update_time": 1535533985333
}, {
"id": 5,
"name": "Feryal",
"update_time": 1535533985444
}],
"page": 0,
"limit": 5,
"hasLoadedAllItems": false
}
现在我们可以确定,如果我们本地数据库中有一个人,其update_time在这个列表的第一个和最后一个人之间,但不在这些人之间,实际上是从远程服务器中删除的因此我们也应该删除它。
我希望我太模糊了,但请看下面的代码
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())
}
})
}
}
但神奇的是发生在 ViewModel 中
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 updatePersons(
persons: List<Person>,
isFirstPage: Boolean,
hasLoadedAllItems: Boolean) {
val minUpdateTime = if (hasLoadedAllItems) {
0
} else {
persons.last().updateTime
}
val maxUpdateTime = if (isFirstPage) {
Long.MAX_VALUE
} else {
persons.first().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 中,我们首先删除从服务器返回的列表中的第一个和最后一个人之间的所有人员,然后将列表插入到数据库中。这样,我们确保在服务器上删除的任何人也会在我们的本地数据库中删除。另请注意,我们在数据库@Transaction 中敲击这两个方法调用以进行更好的优化。数据库的更改将通过我们的 PagedList 发出,从而更新 ui,我们就完成了。