Android PagingLibrary网络+数据库

Android PagingLibrary network + database

我正在开发消息应用程序,我正在实施数据库 + 网络以保存来自 api 的聊天消息并从数据库中显示它们。当数据库没有更多数据时,我正在使用 BoundaryCallback 来获取消息。我的 api 是这样工作的:

getlist(       @Query("msgid") long msgid,
               @Query("loadolder") boolean olderOrNewer,
               @Query("showcurrentMessage") boolean showcurrentMessage,
               @Query("MsgCountToLoad") int MsgCountToLoad);

问题是如何在Pagginglibrary中处理这些东西?如何告诉它加载基于滚动位置的旧消息或新消息。第一次加载数据很容易,它将 returns 空对象,所以我将在下次打开聊天时使用 chat.lastmessageid,我可以检查 chat.lastmessageid 是否等于 db.lastmessageid 和告诉它加载更多新消息。

我正在处理我的最后一个消息应用程序项目。我们在项目中做的最重要和最常见的事情之一是从网络或数据库中逐渐加载数据,这可能是因为有大量实体无法一次加载。

如果您不熟悉分页库或实时数据概念,请先花点时间研究它们,因为我不打算在这里谈论它们。您可以使用很多资源来学习它们。

我的解决方案包括两个主要部分!

  1. 正在使用分页库观察数据库。
  2. 观察 RecyclerView 以了解何时向服务器请求数据页。

为了演示,我们将使用一个实体 class 来表示一个人:

@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 中定义一个方法 returns a DataSource.Factory

@Dao
interface PersonDao {
@Query("SELECT * FROM persons ORDER BY update_time DESC")
fun selectPaged(): DataSource.Factory<Int, Person>
}

现在在我们的 ViewModel 中,我们要从我们的工厂uild 中获取一个 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
}

如您所见,我们将分页与回收器视图绑定在一起,现在我们有三个回调。 isLoading() 应该 return 网络状态。 hasLoadedAllItems() 显示我们是否已到达最后一页并且没有更多数据可从服务器加载。我们所做的大部分工作是实现最后一个方法 onLoadMore()。

在这个阶段我们应该做三件事:

  1. 根据recyclerView的位置,我们要求服务器呈现给我们正确的数据页面。
  2. 我们使用来自服务器的新数据更新数据库,从而更新 PagedList 并显示新数据。别忘了我们正在观察数据库!
  3. 如果请求失败,我们会显示错误。

通过这些简单的步骤,我们解决了两个问题。 首先,尽管有 BoundaryCallbak,但它没有回调来获取已获取的数据,我们正在按需请求每个页面,因此我们可以注意到更新的实体并更新我们自己的本地数据库。 其次,我们可以很容易地显示网络状态,也可以显示可能的网络故障。 听起来不错吧?好吧,我们还没有解决一个特定的问题。这就是如果一个实体从远程服务器中删除的情况。我们怎么会注意到这一点!好吧,这就是数据排序的用武之地。使用一种非常古老的数据排序技巧,我们可以注意到我们之间的差距。例如,如果来自服务器的 returned 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在这个列表的第一个人和最后一个人之间,但不在这些人之中,是在fact 从远程服务器中删除,因此我们也应该删除它。 我希望我说得太含糊了,但请看下面的代码

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 中,我们首先从服务器中删除所有 updateTime 介于列表中第一个和最后一个人之间的人 return,然后将列表插入数据库。这样,我们确保在服务器上删除的任何人在我们的本地数据库中也被删除。另请注意,我们在数据库 @Transaction 中调用这两个方法以实现更好的优化。数据库的更改将通过我们的 PagedList 发出,从而更新 ui 我们就完成了。

PagedList.BoundaryCallback 有两个单独的 API 用于前置和附加。

您应该考虑实施这些方法:

onItemAtEndLoaded
onItemAtFrontLoaded

假设您的初始加载加载的是最新的消息,向上滚动加载的是较旧的消息,您只需传递 true for loadolder in onItemAtFrontLoadedfalse in onItemAtEndLoaded.