3

I am trying to implement paging for the TMDB API using paging3 and paging-compose. Here the source of truth is database and api calls are handled by Remote-mediator.

Repository:

class Repository @Inject constructor(
    val database: Database,
    private val apiService: ApiService
){
    @ExperimentalPagingApi
    fun movies(): Flow<PagingData<Movie>> = Pager(
        config = PagingConfig(pageSize = 20),
        remoteMediator = MovieRemoteMediator(database,apiService),
    ){
       database.movieDao().pagedTopRated()
    }.flow
}

RemoteMediator:

@ExperimentalPagingApi
class MovieRemoteMediator(
    private val database: Database,
    private val networkService: ApiService
) : RemoteMediator<Int, Movie>() {
    override suspend fun load(loadType: LoadType, state: PagingState<Int, Movie>): MediatorResult {
        val page:Int = when(loadType){
            LoadType.REFRESH -> {
                val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
                remoteKeys?.nextKey?.minus(1) ?: 1
            }
            LoadType.PREPEND ->  {
                val remoteKeys = getRemoteKeyForFirstItem(state)
                val prevKey = remoteKeys?.prevKey
                    ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
                prevKey
            }
            LoadType.APPEND -> {
                val remoteKeys = getRemoteKeyForLastItem(state)
                val nextKey = remoteKeys?.nextKey
                    ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
                nextKey
            }
        }
        return try {
            val response
                    : MovieResponse = networkService.getTopRatedMovies(page)
            val toInsert: MutableList<Movie> = mutableListOf();
            for (i in response.results)
                toInsert.add(i.mapToMovie());
            val endOfPaginationReached = response.page + 1 > response.totalPages
            database.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    database.movieKeyDao().clearRemoteKeys()
                    database.movieDao().clearMovies()
                }
                val prevKey = if (page == 1) null else page - 1
                val nextKey = if (endOfPaginationReached) null else page + 1
                val keys = response.results.map {
                    MovieKey(movieId = it.id, prevKey = prevKey, nextKey = nextKey)
                }
                database.movieDao().insertMovies(toInsert)
                database.movieKeyDao().insertAll(keys)
            }
            MediatorResult.Success(
                endOfPaginationReached = endOfPaginationReached
            )
        } catch (e: IOException) {
            MediatorResult.Error(e)
        } catch (e: HttpException) {
            MediatorResult.Error(e)
        }
    }

    private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Movie>): MovieKey? {
        // Get the last page that was retrieved, that contained items.
        // From that last page, get the last item
        return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
            ?.let { repo ->
                // Get the remote keys of the last item retrieved
                database.movieKeyDao().remoteKeysMovieId(repo.id)
            }
    }

    private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Movie>): MovieKey? {
        // Get the first page that was retrieved, that contained items.
        // From that first page, get the first item
        return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
            ?.let { repo ->
                // Get the remote keys of the first items retrieved
                database.movieKeyDao().remoteKeysMovieId(repo.id)
            }
    }

    private suspend fun getRemoteKeyClosestToCurrentPosition(
        state: PagingState<Int, Movie>
    ): MovieKey? {
        // The paging library is trying to load data after the anchor position
        // Get the item closest to the anchor position
        return state.anchorPosition?.let { position ->
            state.closestItemToPosition(position)?.id?.let { repoId ->
                database.movieKeyDao().remoteKeysMovieId(repoId)
            }
        }
    }

ViewModel:

@HiltViewModel
class MovieViewModel @Inject constructor(
    private val movieRepository: Repository
) : ViewModel() {

    @ExperimentalPagingApi
    fun getMovies() = movieRepository.movies().cachedIn(viewModelScope)

}

Ui Screen:

@ExperimentalCoilApi
@ExperimentalPagingApi
@Composable
fun MainScreen(){
    val viewModel: MovieViewModel = viewModel()
    val movieList = viewModel.getMovies().collectAsLazyPagingItems()
    LazyColumn{
        items(movieList){ movie ->
            if (movie != null) {
                Card(movie)
            }
        }
    }
}
@ExperimentalCoilApi
@ExperimentalPagingApi
@Composable
fun Main(){
    MainScreen()
}

MainActivty.kt

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    @ExperimentalCoilApi
    @ExperimentalPagingApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackComposePagingTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Main()
                }
            }
        }
    }
}

I am following the paging3 Codelab for writing the remoteMediator. On opening the app, it only loads the first 2 pages and then loops infinitely making infinite retrofit calls

Isac
  • 314
  • 2
  • 15
  • Debug your solution: what is the difference between page 2 and page 3? Try to inspect the server response, for example by emulating the calls with Postman, or inspect the response by placing breakpoints where you catch exceptions and right after `val response` is created – Nino van Hooff Dec 03 '21 at 15:58
  • How are you navigating to the main screen? Are you sure that MainScreen is not called in a loop? – Sandro Kakhetelidze Dec 14 '21 at 08:35
  • @SandroKakhetelidze I have added the part where i am calling mainscreen – Isac Dec 14 '21 at 09:11
  • [Here](https://medium.com/firebase-tips-tricks/how-to-implement-pagination-in-firestore-using-jetpack-compose-76b4c0b5acd50) is a working solution. – Alex Mamo Jan 24 '22 at 13:48

2 Answers2

2

You doesn't do exactly as The Codelab you're referencing suggests: they are creating paging Flow inside init and only update it when the query string changes.

On the other hand, you're calling viewModel.getMovies() on each recomposition which causes your problem. Check out how you should work with side effects in Compose in documentation.

In this particular case, as you don't have any parameters, you can simply create it once in the view model like this:

@HiltViewModel
class MovieViewModel @Inject constructor(
    private val movieRepository: Repository
) : ViewModel() {
    val moviesPagingFlow = movieRepository.movies().cachedIn(viewModelScope)
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
0

No idea why but in my case isolating the LazyPagingItems from LazyColumn worked.

Try the following:

 @Composable
 fun MainScreen(){
     val viewModel: MovieViewModel = viewModel()
     val movieList = viewModel.getMovies().collectAsLazyPagingItems()
     MainScreen(movies = movieList)
 }

 @Composable
 fun MainScreen(movies: LazyPagingItems<Movie>){
      LazyColumn{
         items(movies){ movie ->
             if (movie != null) {
                 Card(movie)
             }
         }
     }
 }
Thiago Souza
  • 1,171
  • 8
  • 16