【问题标题】:Android Paging Library: How to intelligently switch Between online and offline data?Android分页库:如何智能切换线上线下数据?
【发布时间】:2021-01-13 22:58:32
【问题描述】:

我正在关注 Raywenderlich 在paging-library-for-android-with-kotlin 上关于如何使用 android 分页库的教程。这是网络上最简单的教程之一,我已经彻底遵循了它。但是,我想进行一些更改,以便可以智能地在在线数据和离线数据之间切换。

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

我该怎么做?

我的尝试:

这是我的code on github repository

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

我尝试了一些随机代码,例如创建边界回调。但我不确定如何进行必要的更改。 我不会在这里添加代码(至少现在是这样)以保持简短和准确。

谁能帮帮我?

编辑:

具体来说,我主要从网络加载分页数据。如果出现网络错误,我不想向用户显示错误。相反,我从缓存/数据库中加载分页数据,并尽可能长时间地向我的用户显示。如果网络恢复,则切换回网络分页数据。 (这就是我认为的 instagram / facebook)。实现此目的的适当方法是什么?在答案中查看我的代码/尝试。

【问题讨论】:

  • fox 示例下一个解决方案可能是:如果仅在连接设备时需要从 API 获取数据,请始终从 API 获取数据,如果收到 UnknownHostException 或 IOException,则从你的数据库
  • 我现在看到了您的代码,因此您正在对 Call 进行改造,因此您有一个成功和失败的侦听器方法。失败接收一个可抛出的参数,您需要检查可抛出的类型,如果合适,则从数据库中获取数据。为此,您需要通过构造函数注入数据库。您可以在存储库中查看下一个带有改造和流程的方法(您的方法的一些提示)bitbucket.org/ManuelMato/baseproject/src/develop/app/src/main/…
  • 您链接的教程看起来已经在这样做了。本质上,Room 生成的 DataSource.Factory 将始终从数据库/缓存的离线数据中加载,并触发 BoundaryCallback 从网络中获取项目。这意味着所有的分页都是由本地缓存数据驱动的,这些数据通过 BoundaryCallback 从网络增量更新。您在实施 BoundaryCallback 时遇到什么问题?如果您有一些具体问题,我可以尝试回答。
  • 您是否打算切换顺序以防遇到网络错误并希望优先从数据库加载? BoundaryCallback 不会直接获取要显示的项目,它会将其存储在 DB 中,然后失效以让分页获取新项目,因此它已经实现了这一点,无需任何额外的代码。
  • 明确地说,如果您遇到数据库中存在陈旧数据的问题,我会在您想要刷新时简单地清除数据库。

标签: java android kotlin android-paging android-paging-library


【解决方案1】:

好的,所以在尝试了一些代码 2 天后,这就是我想出的。但是,我真的不知道这是否是一个好习惯。所以我愿意接受任何可接受的答案。

说明:

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

我使用 kotlin 的 Coroutine 以类似 if-else 的简单方式编写了一些异步代码。所以我们可以把它写成这样的伪代码:

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); // <-- submit the data to paging library via callback. this updates your adapter, recyclerview etc...
    }
  }

因此,在 facebook、instagram 等应用中,我们看到它们主要从网络加载数据。但是,如果网络出现故障,他们会向您显示兑现数据。我们可以像这段代码一样智能地进行这个切换。

这里有一段相关代码sn-p,用kotlin写的PageKeyedDataSource:

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.      <-- 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<ProfilePost> = 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<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()) {
        // this means network call is successfull
        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);
        // <-- this is paging library'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);
        pagedProfilePosts.addAll(cachedList);

        if(pagedProfilePosts.size>0) {
          nextDBKey = cachedList.last().id;
        }else{
          nextDBKey = INVALID_KEY;
        }
        nextNetworkKey = INVALID_KEY; // <-- probably there is a network error / sth like that. So no need to execute further network call. thus pass invalid key
        val nextKey: Pair<Long, Long> = 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<ProfilePost> = 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<ProfileServerResponse> = getAPIService().getFeedProfile(
                    sessionToken = authorization, id = userId, withProfile = false, withPosts = true, page = currentNetworkKey.toInt()
            );

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

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

        if(pagedProfilePosts.isNotEmpty()) {
          // this means network call is successfull
          Log.e(TAG, "key -> "+currentNetworkKey+" size -> "+pagedProfilePosts.size+" "+pagedProfilePosts.toString());

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

          pagingLibraryCallBack.onResult(pagedProfilePosts,  nextKey);
          setLoaded();
          // <-- this is paging library'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>0) {
            nextDBKey = cachedList.last().id;
          }else{
            nextDBKey = INVALID_KEY;
          }

          nextNetworkKey = INVALID_KEY; // <-- probably there is a network error / sth like that. So no need to execute further network call. thus pass invalid key
          val nextKey: Pair<Long, Long> = 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;
    }
  }

}

感谢您阅读本文。如果您有更好的解决方案,请随时告诉我。我愿意接受任何可行的解决方案。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-07-23
    • 2012-01-12
    • 1970-01-01
    • 2010-09-23
    • 2011-06-01
    • 2011-06-08
    • 2017-09-09
    相关资源
    最近更新 更多