Android分页库:如何智能切换线上线下数据?
Android Paging Library: How to intelligently switch Between online and offline data?
我正在关注 Raywenderlich 在 paging-library-for-android-with-kotlin 上关于如何使用 android 分页库的教程。这是网上最简单的教程之一,我已经完全遵循了它。但是,我想做一些改变,让我可以智能地在在线数据和离线数据之间切换。
也就是说,我的数据库中有一些旧帖子。最初我有互联网连接。所以我从互联网加载最新数据,然后将其插入我的数据库。最后,我在我的 recyclerView / PagedListAdapter 中显示了这个最新数据。
如果由于某种原因,一段时间后没有互联网连接,我应该显示数据库中的旧帖子。
我该怎么做?
我的尝试:
这是我的code on github repository。
在这里,我尝试创建一个工厂模式。它检查我最初是否有互联网,来自在线数据源的工厂 returns pagedList。否则,来自离线数据源的工厂 returns pagedList。
但这并不能在两种状态之间智能切换。
我尝试了一些随机代码,例如创建边界回调。但我不确定如何进行必要的更改。
我不会在这里添加代码(至少现在)以保持简短和准确。
谁能帮帮我?
编辑:
具体来说,我主要从网络加载分页数据。如果出现网络错误,我不想向用户显示错误。相反,我从缓存/数据库加载分页数据,并尽可能长时间地向我的用户连续显示。如果网络恢复,切换回网络分页数据。 (这就是我认为的 instagram / facebook)。实现这个的合适方法是什么?在答案中查看我的代码/尝试。
好的,所以在尝试了 2 天的一些代码之后,这就是我想出的。但是,我真的不知道这是否是一个好习惯。所以我愿意接受任何可接受的答案。
解释:
由于我有多个数据源(网络和数据库),我在这里创建了ProfilePostDataSource: PageKeyedDataSource<Pair<Long, Long>, ProfilePost>
键是一对,第一个用于网络分页,第二个用于数据库分页。
我使用 kotlin 的 Coroutine 以简单的 if-else 方式编写了一些异步代码。所以我们可以这样写成psudo-code:
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 等应用程序中,我们看到它们主要从网络加载数据。但如果网络出现故障,他们会向您显示已兑现的数据。我们可以像这段代码一样智能地进行这种切换。
这里是一个相关的代码片段,用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;
}
}
}
感谢您阅读到这里。如果您有更好的解决方案,请随时告诉我。我愿意接受任何可行的解决方案。
我正在关注 Raywenderlich 在 paging-library-for-android-with-kotlin 上关于如何使用 android 分页库的教程。这是网上最简单的教程之一,我已经完全遵循了它。但是,我想做一些改变,让我可以智能地在在线数据和离线数据之间切换。
也就是说,我的数据库中有一些旧帖子。最初我有互联网连接。所以我从互联网加载最新数据,然后将其插入我的数据库。最后,我在我的 recyclerView / PagedListAdapter 中显示了这个最新数据。 如果由于某种原因,一段时间后没有互联网连接,我应该显示数据库中的旧帖子。
我该怎么做?
我的尝试:
这是我的code on github repository。
在这里,我尝试创建一个工厂模式。它检查我最初是否有互联网,来自在线数据源的工厂 returns pagedList。否则,来自离线数据源的工厂 returns pagedList。 但这并不能在两种状态之间智能切换。
我尝试了一些随机代码,例如创建边界回调。但我不确定如何进行必要的更改。 我不会在这里添加代码(至少现在)以保持简短和准确。
谁能帮帮我?
编辑:
具体来说,我主要从网络加载分页数据。如果出现网络错误,我不想向用户显示错误。相反,我从缓存/数据库加载分页数据,并尽可能长时间地向我的用户连续显示。如果网络恢复,切换回网络分页数据。 (这就是我认为的 instagram / facebook)。实现这个的合适方法是什么?在答案中查看我的代码/尝试。
好的,所以在尝试了 2 天的一些代码之后,这就是我想出的。但是,我真的不知道这是否是一个好习惯。所以我愿意接受任何可接受的答案。
解释:
由于我有多个数据源(网络和数据库),我在这里创建了ProfilePostDataSource: PageKeyedDataSource<Pair<Long, Long>, ProfilePost>
键是一对,第一个用于网络分页,第二个用于数据库分页。
我使用 kotlin 的 Coroutine 以简单的 if-else 方式编写了一些异步代码。所以我们可以这样写成psudo-code:
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 等应用程序中,我们看到它们主要从网络加载数据。但如果网络出现故障,他们会向您显示已兑现的数据。我们可以像这段代码一样智能地进行这种切换。
这里是一个相关的代码片段,用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;
}
}
}
感谢您阅读到这里。如果您有更好的解决方案,请随时告诉我。我愿意接受任何可行的解决方案。