Android分页库:如何在在线和离线数据之间智能切换?

huangapple go评论53阅读模式
英文:

Android Paging Library: How to intelligently switch Between online and offline data?

问题

我正在按照 Raywenderlich 上的教程,学习如何使用 Android 分页库。这是网络上最简单的教程之一,我已经仔细跟随了它。然而,我想做一些更改,以便我可以在在线数据和离线数据之间进行智能切换。

也就是说,我在数据库中有一些旧的帖子。最初我有互联网连接。因此,我从互联网加载最新数据,然后将其插入到我的数据库中。最后,在我的 recyclerView / PagedListAdapter 中显示这些最新数据。
如果因为某种原因,在一段时间后没有互联网连接,我应该从数据库中显示旧的帖子。

我该如何实现这一点?

我的尝试:

这是我在 GitHub 仓库 上的代码。

在这里,我尝试创建了一个工厂模式。它会检查最初是否有互联网连接,如果有,工厂会从在线数据源返回分页列表。否则,工厂会从离线数据源返回分页列表。
但是这并不能智能地在这两种状态之间切换。

我尝试了一些随机的代码,比如创建边界回调。但我不太清楚如何进行必要的更改。
我暂时不会在这里添加代码,以保持简洁。

有人可以帮帮我吗?

编辑:

更具体地说,我主要从网络加载分页数据。如果出现网络错误,我不想向用户显示错误。相反,我会从缓存/数据库中加载分页数据,并尽可能长时间地向用户显示。如果网络恢复,切换回网络分页数据。(我认为这就是 Instagram / Facebook 所做的)。如何适当地实现这一点?请参阅我的代码/尝试在答案中。

英文:

I am following this tutorial by Raywenderlich on paging-library-for-android-with-kotlin on how to use android paging library. This is one of the easiest tutorials on the net and I have followed it thoroughly. However, I would like to make some changes so that I can intelligently switch between online data and offline data.

That is, I have old some posts in my database. Initially I have internet connection. So I load latest data from internet, then insert it into my database. Finally, I show this latest data in my recyclerView / PagedListAdapter.
If for some reason, there is no internet connection after sometime, I should show the old posts from database.

How can I do this?

My attempts:

This is my code on github repository.

Here, I tried to create a factory pattern. It checks if initially I have internet, the factory returns pagedList from online dataSource. ELse, the factory returns pagedList from offline dataSource.
But this doesnot intelligently switch between the 2 states.

I tried some random codes such as creating a boundary callback. But I am not sure how to make the necessary changes.
I am not adding codes here (at least for now) to keep it short and precise.

Can anyone help me?

Edit:

To be specific, I am loading paged data primarily from network. If there is a network error, I don't want to show the user an error. Instead I load paged data from cache / database and continuously show it to my user as long as possible. If the network is back,switch back to network paged data. (that's what instagram / facebook does I think). What is the appropriate way to implement this? See my code / attemp in the answer.

答案1

得分: 0

好的,以下是翻译好的部分:

解释:

经过两天的代码尝试,我得到了这个结果。但是,我真的不知道这是否是一个好的实践。所以我对任何可接受的答案都持开放态度。

因为我有多个数据源(网络和数据库),我创建了 ProfilePostDataSource: PageKeyedDataSource<Pair<Long, Long>, ProfilePost>,这里的键是一个对,第一个用于网络分页,第二个用于数据库分页。

我使用 Kotlin 的协程以一种类似 if-else 的方式编写了一些异步代码。因此,我们可以将其写成伪代码,如下所示:

Database db;
Retrofit retrofit;

在 loadInitial/loadBefore / loadAfter 中
  currNetworkKey = params.key.first;
  currDBKey = params.key.second;
  
  ArrayList<Model> pagedList;

  协程{
    ArrayList<Model> onlineList = retrofit.getNetworkData(currNetworkKey);  // <-- 我们主要从网络加载数据
    if(onlineList != null) {
      pagedList = onlineList;
      db.insertAll(onlineList);  // <-- 更新缓存
    } else {
      ArrayList<Model> offlineList = db.getOfflineData(currDBKey); // <-- 如果网络失败,我们从数据库加载缓存
      if(offlineList != null) {
           pagedList = offlineList;
      }
    }
    if(pagedList != null 或为空) {
      nextNetworkKey = // 根据需要更新
      nextDBKey = // 根据需要更新
      Pair<int, int> nextKey = new Pair(nextNetworkKey, nextDBKey);
      
      pagingLibraryCallBack.onResult(pagedList, nextKey); // <-- 通过回调将数据提交给分页库。这会更新您的适配器、RecyclerView 等等...
    }
  }

因此,在类似 Facebook、Instagram 等应用中,我们看到它们主要从网络加载数据。但如果网络不可用,它们会显示缓存数据。我们可以像这段代码一样智能地进行切换。

以下是相关的代码片段,用 Kotlin 编写的 PageKeyedDataSource:

ProfilePostDataSource.kt

/** @brief: <Key, Value> = <Integer, ProfilePost>。键 = API 中使用的 pageKey。值 = recyclerView 中的单个项数据类型
 *
 * 我们有一个情况。我们需要第二个 id 来从数据库获取 profilePosts。
 * 更改计划:  <Key, Value> = < Pair<Int, Int>, ProfilePost>。这里
 *
 *                    key.first = API 中使用的 pageKey。      <-- 警告:不要交换这两个!
 *                     Key.second = 数据库中的最后一个项 id,用作我们的数据库分页键
 *
 * Value = recyclerView 中的单个项数据类型
 *
 * */
class ProfilePostDataSource: PageKeyedDataSource<Pair<Long, Long>, ProfilePost> {

  companion object{
    val TAG: String = ProfilePostDataSource::class.java.simpleName;
    val INVALID_KEY: Long = -1;
  }

  private val context: Context;
  private val userId: Int;
  private val liveLoaderState: MutableLiveData<NetworkState>;
  private val profilePostLocalData: ProfilePostLocalDataProvider;

  public constructor(context: Context, userId: Int, profilePostLocalData: ProfilePostLocalDataProvider, liveLoaderState: MutableLiveData<NetworkState>) {
    this.context = context;
    this.userId = userId;
    this.profilePostLocalData = profilePostLocalData;
    this.liveLoaderState = liveLoaderState;
  }

  override fun loadInitial(params: LoadInitialParams<Pair<Long, Long>>, pagingLibraryCallBack: LoadInitialCallback<Pair<Long, Long>, ProfilePost>) {
    val initialNetworkKey: Long = 1L;  // 后缀 = networkKey,因为稍后我们会添加 dbKey
    var nextNetworkKey = initialNetworkKey + 1;
    val prevNetworkKey = null; // 因为在这种情况下我们不会使用它

    val initialDbKey: Long = Long.MAX_VALUE; // 我认为不需要它
    var nextDBKey: Long = 0L;

    GlobalScope.launch(Dispatchers.IO) {
      val pagedProfilePosts: ArrayList<ProfilePost> = ArrayList(); // 因为 kotlin 的 emptyList() 有时会出现奇怪的错误。所以使用 arraylist 并快乐一些
      val authorization : String = AuthManager.getInstance(context).authenticationToken;

      try{
        setLoading();
        val res: Response<ProfileServerResponse> = getAPIService().getFeedProfile(
          sessionToken = authorization, id = userId, withProfile = false, withPosts = true, page = initialNetworkKey.toInt()
        );

        if(res.isSuccessful && res.body()!=null) {
          pagedProfilePosts.addAll(res.body()!!.posts);
        }

      }catch (x: Exception) {
        Log.e(TAG, "Exception -> "+x.message);
      }

      if(pagedProfilePosts.isNotEmpty()) {
        // 这意味着网络调用成功
        Log.e(TAG, "key -> "+initialNetworkKey+" size -> "+pagedProfilePosts.size+" "+pagedProfilePosts.toString());

        nextDBKey = pagedProfilePosts.last().id;
        val nextKey: Pair<Long, Long> = Pair(nextNetworkKey, nextDBKey);

        pagingLibraryCallBack.onResult(pagedProfilePosts, prevNetworkKey, nextKey);
        // <-- 这是分页库的回调,它更新数据,从而更新了 RecyclerView。FeedProfileFragment 中有一行代码:adapter.submitPost(list)。这个回调与那行代码相关...
        profilePostLocalData.insertProfilePosts(pagedProfilePosts, userId); // 将最新的数据插入数据库
      }else{
        // 从缓存中获取数据
        val cachedList: List<ProfilePost> = profilePostLocalData.getProfilePosts(userId);
        pagedProfilePosts.addAll(cachedList);

        if(pagedProfilePosts.size>0) {
          nextDBKey = cachedList.last().id;
        }else{
          nextDBKey = INVALID_KEY;
        }
        nextNetworkKey = INVALID_KEY; // <-- 可能存在网络错误或类似情况。因此无需执行进一步的网络调用。因此传递无效的键
        val nextKey: Pair<Long, Long> = Pair(nextNetworkKey, nextDBKey);
        pagingLibraryCallBack.onResult(pagedProfilePosts, prevNetworkKey, nextKey);

      }
      setLoaded();

    }
  }

  override fun loadBefore(params: LoadParams<Pair<Long, Long>>, paging

<details>
<summary>英文:</summary>

Okay, so after trying out some codes for 2 days, this is what I came up with. However, I really don&#39;t know if this is a good pratice or not. So I am open to any acceptable answers.
 
# Explanation:
Since I have multiple data sources(network and database), I created `ProfilePostDataSource: PageKeyedDataSource&lt;Pair&lt;Long, Long&gt;, ProfilePost&gt;` here the key is a pair, the 1st one for network pagination, the 2nd one is for database pagination.

I used kotlin&#39;s Coroutine to write some asynchronous codes in a simple if-else like manner. So we can write it in a psudo-code like this:

Database db;
Retrofit retrofit;

inside loadInitial/loadBefore / loadAfter:
currNetworkKey = params.key.first;
currDBKey = params.key.second;

ArrayList<Model> pagedList;

coroutine{
ArrayList<Model> onlineList = retrofit.getNetworkData(currNetworkKey); // <-- we primarily load data from network
if(onlineList != null) {
pagedList = onlineList;
db.insertAll(onlineList); // <-- update our cache
}else{
ArrayList<Model> offlineList = db.getOfflineData(currDBKey); // <-- incase the network fails, we load cache from database
if(offlineList !=null){
pagedList = offlineList;
}
}
if(pagedList != null or empty) {
nextNetworkKey = // update it accordingly
nextDBKey = // update it accordingly
Pair<int, int> nextKey = new Pair(nextNetworkKey, nextDBKey);

  pagingLibraryCallBack.onResult(pagedList, nextKey); // &lt;-- submit the data to paging library via callback. this updates your adapter, recyclerview etc...
}

}

So in apps like facebook, instagram etc, we see them primarily loading data from network. But if the network is down, they show you a cashed data. We can intelligently make this switch like this code.
Here is a relevant code snippet, the PageKeyedDataSource written in kotlin:
### ProfilePostDataSource.kt

/** @brief: <Key, Value> = <Integer, ProfilePost>. The key = pageKey used in api. Value = single item data type in the recyclerView
*

  • We have a situation. We need a 2nd id to fetch profilePosts from database.
  • Change of plan: <Key, Value> = < Pair<Int, Int>, ProfilePost>. here the
    *
  •                key.first = pageKey used in api.      &lt;-- Warning: Dont switch these 2!
    
  •                 Key.second = db last items id
    
  •                               used as out db page key
    

*

  • Value = single item data type in the recyclerView
    *
  • */
    class ProfilePostDataSource: PageKeyedDataSource<Pair<Long, Long>, ProfilePost> {

companion object{
val TAG: String = ProfilePostDataSource::class.java.simpleName;
val INVALID_KEY: Long = -1;
}

private val context: Context;
private val userId: Int;
private val liveLoaderState: MutableLiveData<NetworkState>;
private val profilePostLocalData: ProfilePostLocalDataProvider;

public constructor(context: Context, userId: Int, profilePostLocalData: ProfilePostLocalDataProvider, liveLoaderState: MutableLiveData<NetworkState>) {
this.context = context;
this.userId = userId;
this.profilePostLocalData = profilePostLocalData;
this.liveLoaderState = liveLoaderState;
}

override fun loadInitial(params: LoadInitialParams<Pair<Long, Long>>, pagingLibraryCallBack: LoadInitialCallback<Pair<Long, Long>, ProfilePost>) {
val initialNetworkKey: Long = 1L; // suffix = networkKey cz later we'll add dbKey
var nextNetworkKey = initialNetworkKey + 1;
val prevNetworkKey = null; // cz we wont be using it in this case

val initialDbKey: Long = Long.MAX_VALUE; // dont think I need it
var nextDBKey: Long = 0L;
GlobalScope.launch(Dispatchers.IO) {
val pagedProfilePosts: ArrayList&lt;ProfilePost&gt; = ArrayList(); // cz kotlin emptyList() sometimes gives a weird error. So use arraylist and be happy
val authorization : String = AuthManager.getInstance(context).authenticationToken;
try{
setLoading();
val res: Response&lt;ProfileServerResponse&gt; = getAPIService().getFeedProfile(
sessionToken = authorization, id = userId, withProfile = false, withPosts = true, page = initialNetworkKey.toInt()
);
if(res.isSuccessful &amp;&amp; res.body()!=null) {
pagedProfilePosts.addAll(res.body()!!.posts);
}
}catch (x: Exception) {
Log.e(TAG, &quot;Exception -&gt; &quot;+x.message);
}
if(pagedProfilePosts.isNotEmpty()) {
// this means network call is successfull
Log.e(TAG, &quot;key -&gt; &quot;+initialNetworkKey+&quot; size -&gt; &quot;+pagedProfilePosts.size+&quot; &quot;+pagedProfilePosts.toString());
nextDBKey = pagedProfilePosts.last().id;
val nextKey: Pair&lt;Long, Long&gt; = Pair(nextNetworkKey, nextDBKey);
pagingLibraryCallBack.onResult(pagedProfilePosts, prevNetworkKey, nextKey);
// &lt;-- this is paging library&#39;s callback to a pipeline that updates data which inturn updates the recyclerView. There is a line: adapter.submitPost(list) in FeedProfileFragment. this callback is related to that line...
profilePostLocalData.insertProfilePosts(pagedProfilePosts, userId); // insert the latest data in db
}else{
// fetch data from cache
val cachedList: List&lt;ProfilePost&gt; = profilePostLocalData.getProfilePosts(userId);
pagedProfilePosts.addAll(cachedList);
if(pagedProfilePosts.size&gt;0) {
nextDBKey = cachedList.last().id;
}else{
nextDBKey = INVALID_KEY;
}
nextNetworkKey = INVALID_KEY; // &lt;-- probably there is a network error / sth like that. So no need to execute further network call. thus pass invalid key
val nextKey: Pair&lt;Long, Long&gt; = Pair(nextNetworkKey, nextDBKey);
pagingLibraryCallBack.onResult(pagedProfilePosts, prevNetworkKey, nextKey);
}
setLoaded();
}

}

override fun loadBefore(params: LoadParams<Pair<Long, Long>>, pagingLibraryCallBack: LoadCallback<Pair<Long, Long>, ProfilePost>) {} // we dont need it in feedProflie

override fun loadAfter(params: LoadParams<Pair<Long, Long>>, pagingLibraryCallBack: LoadCallback<Pair<Long, Long>, ProfilePost>) {
val currentNetworkKey: Long = params.key.first;
var nextNetworkKey = currentNetworkKey; // assuming invalid key
if(nextNetworkKey!= INVALID_KEY) {
nextNetworkKey = currentNetworkKey + 1;
}

val currentDBKey: Long = params.key.second;
var nextDBKey: Long = 0;
if(currentDBKey!= INVALID_KEY || currentNetworkKey!= INVALID_KEY) {
GlobalScope.launch(Dispatchers.IO) {
val pagedProfilePosts: ArrayList&lt;ProfilePost&gt; = ArrayList(); // cz kotlin emptyList() sometimes gives a weird error. So use arraylist and be happy
val authorization : String = AuthManager.getInstance(context).authenticationToken;
try{
setLoading();
if(currentNetworkKey!= INVALID_KEY) {
val res: Response&lt;ProfileServerResponse&gt; = getAPIService().getFeedProfile(
sessionToken = authorization, id = userId, withProfile = false, withPosts = true, page = currentNetworkKey.toInt()
);
if(res.isSuccessful &amp;&amp; res.body()!=null) {
pagedProfilePosts.addAll(res.body()!!.posts);
}
}
}catch (x: Exception) {
Log.e(TAG, &quot;Exception -&gt; &quot;+x.message);
}
if(pagedProfilePosts.isNotEmpty()) {
// this means network call is successfull
Log.e(TAG, &quot;key -&gt; &quot;+currentNetworkKey+&quot; size -&gt; &quot;+pagedProfilePosts.size+&quot; &quot;+pagedProfilePosts.toString());
nextDBKey = pagedProfilePosts.last().id;
val nextKey: Pair&lt;Long, Long&gt; = Pair(nextNetworkKey, nextDBKey);
pagingLibraryCallBack.onResult(pagedProfilePosts,  nextKey);
setLoaded();
// &lt;-- this is paging library&#39;s callback to a pipeline that updates data which inturn updates the recyclerView. There is a line: adapter.submitPost(list) in FeedProfileFragment. this callback is related to that line...
profilePostLocalData.insertProfilePosts(pagedProfilePosts, userId); // insert the latest data in db
}else{
// fetch data from cache

// val cachedList: List<ProfilePost> = profilePostLocalData.getProfilePosts(userId);
val cachedList: List<ProfilePost> = profilePostLocalData.getPagedProfilePosts(userId, nextDBKey, 20);
pagedProfilePosts.addAll(cachedList);

      if(pagedProfilePosts.size&gt;0) {
nextDBKey = cachedList.last().id;
}else{
nextDBKey = INVALID_KEY;
}
nextNetworkKey = INVALID_KEY; // &lt;-- probably there is a network error / sth like that. So no need to execute further network call. thus pass invalid key
val nextKey: Pair&lt;Long, Long&gt; = Pair(nextNetworkKey, nextDBKey);
pagingLibraryCallBack.onResult(pagedProfilePosts, nextKey);
setLoaded();
}
}
}

}

private suspend fun setLoading() {
withContext(Dispatchers.Main) {
liveLoaderState.value = NetworkState.LOADING;
}
}

private suspend fun setLoaded() {
withContext(Dispatchers.Main) {
liveLoaderState.value = NetworkState.LOADED;
}
}

}


Thank you for reading this far. If you have a better solution, feel free to let me know. I&#39;m open to any working solutions.
</details>

huangapple
  • 本文由 发表于 2020年9月28日 18:56:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/64100844.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定