如何在使用 Paging Library 中的 Remote Mediator 时创建自己的分页数据源?

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

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&lt;Int, Film&gt;() {

    override fun getRefreshKey(state: PagingState&lt;Int, Film&gt;): Int? {
        return state.anchorPosition?.let { anchorPosition -&gt;
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams&lt;Int&gt;): LoadResult&lt;Int, Film&gt; {
        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() &lt; 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&lt;Int, Film&gt;() {

    override suspend fun load(loadType: LoadType, state: PagingState&lt;Int, Film&gt;): MediatorResult {
        return try{
            val loadKey = when (loadType) {
                LoadType.REFRESH -&gt; {
                    1
                }
                LoadType.PREPEND -&gt; {
                    return MediatorResult.Success(endOfPaginationReached = true)
                }
                LoadType.APPEND -&gt; {
                    last += 1
                    last
                }
            }
            val films = when(type){
                TopFilmCategories.TOP_100_POPULAR_FILMS -&gt; filmRemoteStorage.getPopularFilms(loadKey)
                TopFilmCategories.TOP_250_BEST_FILMS -&gt; filmRemoteStorage.getBestFilms(loadKey)
                TopFilmCategories.TOP_AWAIT_FILMS -&gt; 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&lt;PagingData&lt;DomainFilm&gt;&gt; {
        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&lt;PagingData&lt;com.gramzin.cinescope.data.model.Film&gt;&gt;.toDomain(): Flow&lt;PagingData&lt;DomainFilm&gt;&gt; {
    return transform { value -&gt;
        emit(value.map {
            it.toDomain()
        })
    }
}

I tried to log the actions that occur:

  1. paging source: load page 1

  2. remote mediator: refresh operation (load page 1)

  3. paging source: load page 2

  4. remote mediator: load success

  5. remote mediator: prepend operation

  6. remote mediator: append operation (load page 2)

  7. 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&lt;Int, Film&gt;() {

private val observer = ThreadSafeInvalidationObserver(
    tables = arrayOf(DBUtils.filmsTableName),
    onInvalidated = ::invalidate
)

override fun getRefreshKey(state: PagingState&lt;Int, Film&gt;): Int? {
    return state.anchorPosition?.let { anchorPosition -&gt;
        val anchorPage = state.closestPageToPosition(anchorPosition)
        anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
}

override suspend fun load(params: LoadParams&lt;Int&gt;): LoadResult&lt;Int, Film&gt; {
    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() &lt; 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&lt;Int, Film&gt;() {

     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&lt;String&gt;) {
                 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&lt;Int, Film&gt;): Int? {
         return state.anchorPosition?.let { anchorPosition -&gt;
             val anchorPage = state.closestPageToPosition(anchorPosition)
             anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
         }
     }

     override suspend fun load(params: LoadParams&lt;Int&gt;): LoadResult&lt;Int, Film&gt; {
    
         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() &lt; count) null else currentPage + 1

             LoadResult.Page(films, prevKey, nextKey)
         } catch (ex: Exception){
             LoadResult.Error(ex)
         }
     }
}

huangapple
  • 本文由 发表于 2023年4月6日 22:46:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/75950852.html
匿名

发表评论

匿名网友

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

确定