Keeping loaded pages in lazyColumn while PagingData invalidated by the Mediator

71 views Asked by At

I am using paging3 to fetch data from api save it into the database and finally show them to the user. I have seen the wired item count while collecting from the flow :

Here is my Pager config :

override suspend fun getAllMovie(): Flow<PagingData<MovieEntity>> {
        val pagingSourceFactory = { appDatabase.movieDao().pagingSource() }


        return Pager(config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false),
                remoteMediator = MovieRemoteMediator(mContext,movieRemoteDataSource, appDatabase),
                pagingSourceFactory = pagingSourceFactory)
            .flow
    }

Then in the use case I map it :

class GetMoviesUseCase(private val repository: MovieRepository) : BaseUseCase<PagingData<Movie>> {
    val list : MutableList<Movie> = ArrayList<Movie>()
    override suspend fun invoke(): Flow<PagingData<Movie>> {
        return  repository.getAllMovie()
              .map { value ->
                  value.map { entity ->
                      entity.toMovie()
                  } }
    }
}

After that in ViewModel I cache it :

private fun getAllMovies() {
        viewModelScope.launch {
            val pagedFlow = getMoviesUseCase()
                .cachedIn(scope = viewModelScope)
            setState { currentState.copy( pagedData = pagedFlow) }
        }
    }

Finally in screen, I use it :

    var pagingItems: LazyPagingItems<Movie>? = null
    viewState.pagedData?.let { flowPage ->
        pagingItems = flowPage.collectAsLazyPagingItems()
        Log.d("MovieScreen", "  Init pagingItems - count: ${pagingItems?.itemCount} ")
    }

As you can see I created a log for 'pagingItems - count' . Here as we expected the count should goes up as we scroll the list and get more pages . but for me it's not .

see the logs : 

Init pagingItems - count: 0 
Init pagingItems - count: 60 
Init pagingItems - count: 60 
Init pagingItems - count: 20 
Init pagingItems - count: 40 
Init pagingItems - count: 60 
Init pagingItems - count: 80 
Init pagingItems - count: 60 
Init pagingItems - count: 74 
Init pagingItems - count: 60 
Init pagingItems - count: 77 
Init pagingItems - count: 60 
Init pagingItems - count: 80 
Init pagingItems - count: 100 
Init pagingItems - count: 117 
Init pagingItems - count: 60 
Init pagingItems - count: 80 
Init pagingItems - count: 100 
Init pagingItems - count: 105 
Init pagingItems - count: 60 

For more detail of my code I would add my mediator and the repository for better understanding of my code:

@OptIn(ExperimentalPagingApi::class)
class MovieRemoteMediator(
        private val  mContext: Context,
        private val service: MovieRemoteDataSource,
                          private val appDatabase: AppDatabase
) : RemoteMediator<Int, MovieEntity>() {

    override suspend fun initialize(): InitializeAction {
        return InitializeAction.LAUNCH_INITIAL_REFRESH
    }

    override suspend fun load(loadType: LoadType,
                              state: PagingState<Int, MovieEntity>): MediatorResult {
        Log.d(TAG, "loadType: $loadType")
        val page = when (loadType) {
            LoadType.REFRESH -> {
                val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
                remoteKeys?.nextKey?.minus(1) ?: STARTING_PAGE_INDEX
            }

            LoadType.PREPEND -> {
                val remoteKeys = getRemoteKeyForFirstItem(state)

                val prevKey = remoteKeys?.prevKey
                    ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
                Log.d(TAG, "LoadType.PREPEND - prevKey: $prevKey")
                prevKey
            }

            LoadType.APPEND -> {
                val remoteKeys = getRemoteKeyForLastItem(state)
                val nextKey = remoteKeys?.nextKey
                    ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)

                Log.d(TAG, "LoadType.APPEND - nextKey: $nextKey")
                nextKey
            }
        }


        try {
            val apiResponse = service.getAllMovies(page)

            val movieDataList = apiResponse.body()?.results
            val endOfPaginationReached = movieDataList?.isEmpty()

            appDatabase.withTransaction {
                // clear all tables in the database
                if (loadType == LoadType.REFRESH) {
                    appDatabase.remoteKeysDao().clearRemoteKeys()
                    appDatabase.movieDao().clearRepos()
                }

                val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1
                val nextKey = if (endOfPaginationReached == true) null else page + 1
                Log.d(TAG, "New Data - nextKey: $nextKey - prevKey: $prevKey ")

                movieDataList?.let {
                    val insertedIds = appDatabase.movieDao().insertAll(it.toMovieList())
                    val keys = insertedIds.map { resultItem ->
                        RemoteKeysEntity(movieId = resultItem.toInt(), prevKey = prevKey,
                                nextKey = nextKey)
                    }
                    appDatabase.remoteKeysDao().insertAll(keys)
                }
            }
            return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached == true)
        } catch (exception: IOException) {
            return MediatorResult.Error(exception)
        } catch (exception: HttpException) {
            return MediatorResult.Error(exception)
        }
    }

    private suspend fun getRemoteKeyClosestToCurrentPosition(
            state: PagingState<Int, MovieEntity>
    ): RemoteKeysEntity? {
        // 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 ->
                appDatabase.remoteKeysDao().remoteKeysMovieId(repoId)
            }
        }
    }

    private suspend fun getRemoteKeyForFirstItem(
            state: PagingState<Int, MovieEntity>): RemoteKeysEntity? {
        // 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 { movie ->
                // Get the remote keys of the first items retrieved
                appDatabase.remoteKeysDao().remoteKeysMovieId(movie.id)
            }
    }

    private suspend fun getRemoteKeyForLastItem(
            state: PagingState<Int, MovieEntity>): RemoteKeysEntity? {
        // 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
                appDatabase.remoteKeysDao().remoteKeysMovieId(repo.id)
            }
    }

    companion object {
        private const val STARTING_PAGE_INDEX = 1
        private const val TAG = "MovieRemoteMediator"
    }
}

And the repository :

class MovieRepositoryImpl @Inject constructor(
        private val  mContext: Context,
        private val movieRemoteDataSource: MovieRemoteDataSource,
        private val appDatabase: AppDatabase,

        ) : MovieRepository {
    @OptIn(ExperimentalPagingApi::class)
    override suspend fun getAllMovie(): Flow<PagingData<MovieEntity>> {
        val pagingSourceFactory = { appDatabase.movieDao().pagingSource() }


        return Pager(config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false),
                remoteMediator = MovieRemoteMediator(mContext,movieRemoteDataSource, appDatabase),
                pagingSourceFactory = pagingSourceFactory)
            .flow
    }
}

I would greatly appreciate the assistance of those who can help me overcome my current challenge.

I have checked my paging3 implementation with Google sample in the link, But I can't find the solution.

As Update:

I found that a thing :) In the single source of truth, the database is responsible for serving data into the list. Let's imagine in db we have just 3 pages, So we loaded those 3 pages, Then we have to load more with MovieRemoteMediator. In MovieRemoteMediator after getting a response we insert the new data into db. I think it causes the problem that I have. Because on the other hand, we are observing the database. If changes happen it would change also our list. Here is the code :

    try {
            val apiResponse = service.getAllMovies(page)

            val movieDataList = apiResponse.body()?.results
            val endOfPaginationReached = movieDataList?.isEmpty()

            appDatabase.withTransaction {
                // clear all tables in the database
                if (loadType == LoadType.REFRESH) {
                    appDatabase.remoteKeysDao().clearRemoteKeys()
                    appDatabase.movieDao().clearRepos()
                }

                val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1
                val nextKey = if (endOfPaginationReached == true) null else page + 1
                Log.d(TAG, "New Data - nextKey: $nextKey - prevKey: $prevKey ")

                movieDataList?.let {
                    val insertedIds = appDatabase.movieDao().insertAll(it.toMovieList())
                    val keys = insertedIds.map { resultItem ->
                        RemoteKeysEntity(movieId = resultItem.toInt(), prevKey = prevKey,
                                nextKey = nextKey)
                    }
                    appDatabase.remoteKeysDao().insertAll(keys)
                }
            }
            return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached == true)
0

There are 0 answers