英文:
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'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<Pair<Long, Long>, ProfilePost>` here the key is a pair, the 1st one for network pagination, the 2nd one is for database pagination.
I used kotlin'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); // <-- 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. <-- 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;
}
}
}
Thank you for reading this far. If you have a better solution, feel free to let me know. I'm open to any working solutions.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论