英文:
How do I make my own Paging Source when using Remote Mediator in the Paging Library?
问题
我尝试制作自己的分页源,因为 Room 库提供的分页源不适合我。我遇到的问题是,分页源加载了第1或2页的数据(取决于远程中介是否有时间删除刷新操作的数据库数据),当向下滚动这些页面时,不再加载数据。
我认为问题在于分页源不知道远程中介已从 API 下载了新数据。我该如何解决这个问题?
class TopFilmsLocalPagingSource(
private val filmLocalStorage: FilmLocalStorage,
private val type: TopFilmCategories
): PagingSource<Int, Film>() {
// ...(你的代码)
}
@OptIn(ExperimentalPagingApi::class)
class FilmRemoteMediator(
private val filmLocalStorage: FilmLocalStorage,
private val filmRemoteStorage: FilmRemoteStorage,
private val type: TopFilmCategories
): RemoteMediator<Int, Film>() {
// ...(你的代码)
}
class FilmRepositoryImpl @Inject constructor(
private val filmRemoteStorage: FilmRemoteStorage,
private val filmLocalStorage: FilmLocalStorage
): FilmRepository {
// ...(你的代码)
}
英文:
I am trying to make my own Paging Source, because the Paging Source provided by the Room library does not suit me. I ran into such a problem that Paging Source loads the first 1 or 2 pages of data (depending on whether Remote Mediator has time to delete data from the database for the Refresh operation), when scrolling these pages down, data is no longer loaded.
I think the problem is that Paging Source does not understand that Remote Mediator has downloaded new data from the API. How do I solve this problem?
Paging Source:
class TopFilmsLocalPagingSource(
private val filmLocalStorage: FilmLocalStorage,
private val type: TopFilmCategories): PagingSource<Int, Film>() {
override fun getRefreshKey(state: PagingState<Int, Film>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Film> {
val currentPage = params.key ?: 1
val offset = (currentPage - 1) * params.loadSize
val count = params.loadSize
return try {
val films = filmLocalStorage.getFilmsByType(offset, count, type)
val prevKey = if (currentPage == 1) null else currentPage - 1
val nextKey = if (films.count() < count) null else currentPage + 1
LoadResult.Page(films, prevKey, nextKey)
} catch (ex: Exception){
LoadResult.Error(ex)
}
}
}
Remote Mediator (the last field in companion object is made for testing):
@OptIn(ExperimentalPagingApi::class)
class FilmRemoteMediator(
private val filmLocalStorage: FilmLocalStorage,
private val filmRemoteStorage: FilmRemoteStorage,
private val type: TopFilmCategories): RemoteMediator<Int, Film>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, Film>): MediatorResult {
return try{
val loadKey = when (loadType) {
LoadType.REFRESH -> {
1
}
LoadType.PREPEND -> {
return MediatorResult.Success(endOfPaginationReached = true)
}
LoadType.APPEND -> {
last += 1
last
}
}
val films = when(type){
TopFilmCategories.TOP_100_POPULAR_FILMS -> filmRemoteStorage.getPopularFilms(loadKey)
TopFilmCategories.TOP_250_BEST_FILMS -> filmRemoteStorage.getBestFilms(loadKey)
TopFilmCategories.TOP_AWAIT_FILMS -> filmRemoteStorage.getTopAwaitFilms(loadKey)
}
if (loadType == LoadType.REFRESH) {
filmLocalStorage.refreshFilmsByType(films, type)
MediatorResult.Success(
endOfPaginationReached = films.isEmpty()
)
}
else{
filmLocalStorage.insertAllFilms(films, type)
MediatorResult.Success(
endOfPaginationReached = films.isEmpty()
)
}
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
companion object{
var last = 1
}
}
Repository:
class FilmRepositoryImpl @Inject constructor(
private val filmRemoteStorage: FilmRemoteStorage,
private val filmLocalStorage: FilmLocalStorage): FilmRepository {
@OptIn(ExperimentalPagingApi::class)
override fun getBestFilmsPaged(): Flow<PagingData<DomainFilm>> {
return Pager(PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 20),
remoteMediator = FilmRemoteMediator(filmLocalStorage,
filmRemoteStorage, TopFilmCategories.TOP_250_BEST_FILMS)){
TopFilmsLocalPagingSource(filmLocalStorage, TopFilmCategories.TOP_250_BEST_FILMS)
}.flow.toDomain()
}
}
fun Flow<PagingData<com.gramzin.cinescope.data.model.Film>>.toDomain(): Flow<PagingData<DomainFilm>> {
return transform { value ->
emit(value.map {
it.toDomain()
})
}
}
I tried to log the actions that occur:
-
paging source: load page 1
-
remote mediator: refresh operation (load page 1)
-
paging source: load page 2
-
remote mediator: load success
-
remote mediator: prepend operation
-
remote mediator: append operation (load page 2)
-
remote mediator: load success
答案1
得分: 0
需要添加ThreadSafeInvalidationObserver,以跟踪数据库中的更改。您还需要在load方法中从观察者调用registerIfNecessary方法。
class TopFilmsLocalPagingSource(
private val filmLocalStorage: FilmLocalStorage,
private val type: TopFilmCategories): PagingSource<Int, Film>() {
private val observer = ThreadSafeInvalidationObserver(
tables = arrayOf(DBUtils.filmsTableName),
onInvalidated = ::invalidate
)
override fun getRefreshKey(state: PagingState<Int, Film>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Film> {
observer.registerIfNecessary(filmLocalStorage.db)
val currentPage = params.key ?: 1
val offset = (currentPage - 1) * params.loadSize
val count = params.loadSize
return try {
val films = filmLocalStorage.getFilmsByType(offset, count, type)
val prevKey = if (currentPage == 1) null else currentPage - 1
val nextKey = if (films.count() < count) null else currentPage + 1
LoadResult.Page(films, prevKey, nextKey)
} catch (ex: Exception){
LoadResult.Error(ex)
}
}
}
英文:
You need to add a ThreadSafeInvalidationObserver that will track changes in the database. You also need to call the registerIfNecessary method from the observer in the load method.
class TopFilmsLocalPagingSource(
private val filmLocalStorage: FilmLocalStorage,
private val type: TopFilmCategories): PagingSource<Int, Film>() {
private val observer = ThreadSafeInvalidationObserver(
tables = arrayOf(DBUtils.filmsTableName),
onInvalidated = ::invalidate
)
override fun getRefreshKey(state: PagingState<Int, Film>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Film> {
observer.registerIfNecessary(filmLocalStorage.db)
val currentPage = params.key ?: 1
val offset = (currentPage - 1) * params.loadSize
val count = params.loadSize
return try {
val films = filmLocalStorage.getFilmsByType(offset, count, type)
val prevKey = if (currentPage == 1) null else currentPage - 1
val nextKey = if (films.count() < count) null else currentPage + 1
LoadResult.Page(films, prevKey, nextKey)
} catch (ex: Exception){
LoadResult.Error(ex)
}
}
}}
答案2
得分: 0
ThreadSafeInvalidationObserver 在最新版本的 Room 中似乎不存在。以下是另一种添加失效观察者的方法:
class TopFilmsLocalPagingSource(
private val filmLocalStorage: FilmLocalStorage,
private val type: TopFilmCategories
) : PagingSource<Int, Film>() {
init {
val db = // 在这里添加数据库实例
db.invalidationTracker.addObserver(object :
InvalidationTracker.Observer(arrayOf(在这里添加你的表名)) {
override fun onInvalidated(tables: Set<String>) {
this@TopFilmsLocalPagingSource.invalidate()
// 移除观察者,因为在调用 invalidate 时 pager 会创建分页源的新实例
db.invalidationTracker.removeObserver(this)
}
})
}
override fun getRefreshKey(state: PagingState<Int, Film>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Film> {
val currentPage = params.key ?: 1
val offset = (currentPage - 1) * params.loadSize
val count = params.loadSize
return try {
val films = filmLocalStorage.getFilmsByType(offset, count, type)
val prevKey = if (currentPage == 1) null else currentPage - 1
val nextKey = if (films.count() < count) null else currentPage + 1
LoadResult.Page(films, prevKey, nextKey)
} catch (ex: Exception) {
LoadResult.Error(ex)
}
}
}
英文:
It seems that the ThreadSafeInvalidationObserver does not exists, at least in the latest version of room.
Here is an other way to add an inavalidation observer
class TopFilmsLocalPagingSource(
private val filmLocalStorage: FilmLocalStorage,
private val type: TopFilmCategories): PagingSource<Int, Film>() {
init {
val db = //you need the instance of your database here
db.invalidationTracker.addObserver(object : InvalidationTracker.Observer(arrayOf(add here your table name)) {
override fun onInvalidated(tables: Set<String>) {
this@TopFilmsLocalPagingSource.invalidate()
//Remove this observer because when the invalidate is invoked pager creates a new instance of the paging source
db.invalidationTracker.removeObserver(this)
}
})
}
override fun getRefreshKey(state: PagingState<Int, Film>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Film> {
val currentPage = params.key ?: 1
val offset = (currentPage - 1) * params.loadSize
val count = params.loadSize
return try {
val films = filmLocalStorage.getFilmsByType(offset, count, type)
val prevKey = if (currentPage == 1) null else currentPage - 1
val nextKey = if (films.count() < count) null else currentPage + 1
LoadResult.Page(films, prevKey, nextKey)
} catch (ex: Exception){
LoadResult.Error(ex)
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论