1

i met a problem using pager3 library
my fetching data flow like below

activity(composable) -> viewmodel -> usecase(datatransformation) -> repository(pager) -> remotemediator


sorry for [result image][1] link

if i assume paging size is N (in above image it is 3)
then it automatically requests multiple times(t) and stop

data from network accurately saved in database N*t
but as you can see in result image, the last pageData N items are null(from lazypagingitems snapshot from flow, size is correctly come but last N items null)
and then they stop calling mediator's load function regardless of scrolling end of recyclerview
pleas beat me


MainActivity
val pagingVideos = mainViewModel.youtubeVideoList.collectAsLazyPagingItems()
                .apply {
                    if (!itemSnapshotList.isNullOrEmpty())
                        Timber.i("${itemSnapshotList.last()?.title}")
                    when (loadState.refresh) {
                        is LoadState.Error -> {
                            when ((loadState.refresh as LoadState.Error).error) {
                                is NetworkCustomException.AccessTokenInvalidException -> signOut()
                            }
                        }
                        else -> {

                        }
                    }
                }
...
YoutubePagingList(lazyYoutubeMostPopularVideoList = pagingVideos)
...
LazyColumn(
        contentPadding = PaddingValues(horizontal = 10.dp, vertical = 10.dp)
    ) {
        if (!lazyYoutubeMostPopularVideoList.itemSnapshotList.isEmpty()) {
            Timber.i("${lazyYoutubeMostPopularVideoList.itemSnapshotList.last()}")
            items(lazyYoutubeMostPopularVideoList.itemSnapshotList) { youtube ->
                if (youtube != null)
                    YoutubeRow(youtube = youtube)
                else
                    YoutubeRowPlaceHolder()
            }
        }
    }

ViewModel
    var _youtubeVideoList: Flow<PagingData<YoutubeMostPopularVideoUseCaseItem>> =
        youtubeMostPopularVideoUseCase.searchMostPopularVideos("").cachedIn(viewModelScope)

    val youtubeVideoList: Flow<PagingData<YoutubeMostPopularVideoUseCaseItem>>
        get() = _youtubeVideoList

UseCase
class YoutubeMostPopularVideoUseCase @Inject constructor(private val youtubeRepository: YoutubeRepository) {

    fun searchMostPopularVideos(query: String): Flow<PagingData<YoutubeMostPopularVideoUseCaseItem>> {
        return youtubeRepository.searchMostPopularVideos(query).map {
            it.map { it2 ->
                YoutubeMostPopularVideoUseCaseItem(
                    thumbnailUrl = it2.thumbnailUrl,
                    title = it2.title,
                    description = it2.description
                )
            }
        }
    }
}

data class YoutubeMostPopularVideoUseCaseItem(
    val thumbnailUrl: String,
    val title: String,
    val description: String
)

Repository
@Singleton
@OptIn(ExperimentalPagingApi::class)
class YoutubeRepository @Inject constructor(
    private val youtubeDatabase: YoutubeDatabase
) {
    fun searchMostPopularVideos(query: String): Flow<PagingData<YoutubeMostPopularVideoEntity>> {
        try {
            return Pager(
                config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = true),
                remoteMediator = YoutubeRemoteMediator(youtubeDatabase)
            ) {
                val dbQuery = "%${query.replace(' ', '%')}%"
                youtubeDatabase.youtubeMostPopularVideoDao.pagingSource(dbQuery)
            }.flow
        } catch (e: Exception) {
            throw e
        }
    }
}

Mediator
@OptIn(ExperimentalPagingApi::class)
class YoutubeRemoteMediator @Inject constructor(
    @ApplicationContext private val youtubeDatabase: YoutubeDatabase
) : RemoteMediator<Int, YoutubeMostPopularVideoEntity>() {

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, YoutubeMostPopularVideoEntity>
    ): MediatorResult {
        return try {
            val loadKey = when (loadType) {
                LoadType.REFRESH -> {
                    val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
                    remoteKeys?.currKey
                }
                LoadType.PREPEND -> {
                    val remoteKeys = getRemoteKeyForFirstItem(state)
                    val prevKey = remoteKeys?.prevKey

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

            Timber.i("loadkey:  ${loadKey}")
            val response =
                NetworkManager.youtubeService.searchMostPopularVideos(pageToken = loadKey)

            youtubeDatabase.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    youtubeDatabase.youtubeMostPopularVideoRemoteKeyDao.clearAll()
                    youtubeDatabase.youtubeMostPopularVideoDao.clearAll()
                }

                when (response) {
                    is NetworkResponse.Success -> {
                        CoroutineScope(Dispatchers.IO).launch {
                            youtubeDatabase.youtubeMostPopularVideoRemoteKeyDao.insertOrReplace(
                                response.body.items.map { it ->
                                    YoutubeMostPopularVideoRemoteKeyEntity(
                                        it.id,
                                        response.body.prevPageToken,
                                        loadKey,
                                        response.body.nextPageToken
                                    )
                                }
                            )
                            Timber.i("next PageToken : ${response.body.nextPageToken}")
                            youtubeDatabase.youtubeMostPopularVideoDao.insertAll(response.body.items.asDatabaseModel())
                        }
                        MediatorResult.Success(
                            endOfPaginationReached = response.body.nextPageToken == null
                        )
                    }
                    else -> MediatorResult.Error(response.getCustomException())

                }
            }
        } catch (e: Exception) {
            Timber.i("error $e")
            MediatorResult.Error(e)
        }
    }

    private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, YoutubeMostPopularVideoEntity>): YoutubeMostPopularVideoRemoteKeyEntity? {
        return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
            ?.let { video ->
                Timber.i("remoteKeyForLastItem")
                youtubeDatabase.youtubeMostPopularVideoRemoteKeyDao.remoteKeyByQuery(video.id)
            }
    }

    private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, YoutubeMostPopularVideoEntity>): YoutubeMostPopularVideoRemoteKeyEntity? {
        return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
            ?.let { video ->
                Timber.i("remoteKeyForFirstItem")
                youtubeDatabase.youtubeMostPopularVideoRemoteKeyDao.remoteKeyByQuery(video.id)
            }
    }

    private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, YoutubeMostPopularVideoEntity>): YoutubeMostPopularVideoRemoteKeyEntity? {
        return state.anchorPosition?.let { position ->
            state.closestItemToPosition(position)?.id?.let { videoId ->
                Timber.i("remoteKeyForClosestItem")
                youtubeDatabase.youtubeMostPopularVideoRemoteKeyDao.remoteKeyByQuery(videoId)
            }
        }
    }
}

Dao

@Dao
interface YoutubeMostPopularVideoDao {
    @Query("select * from youtube_most_popular_video")
    fun getVideos(): LiveData<List<YoutubeMostPopularVideoEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(videos: List<YoutubeMostPopularVideoEntity>)

    @Query("SELECT * FROM youtube_most_popular_video WHERE id LIKE :query")
    fun pagingSource(query: String): PagingSource<Int, YoutubeMostPopularVideoEntity>

    @Query("DELETE FROM youtube_most_popular_video")
    suspend fun clearAll()
}

@Dao
interface YoutubeMostPopularVideoRemoteKeyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrReplace(youtubeMostPopularVideoRemoteKeyEntity: List<YoutubeMostPopularVideoRemoteKeyEntity>)

    @Query("SELECT * FROM youtube_most_popular_video_remote_key WHERE id = :id")
    suspend fun remoteKeyByQuery(id: String): YoutubeMostPopularVideoRemoteKeyEntity

    @Query("DELETE FROM youtube_most_popular_video_remote_key")
    suspend fun clearAll()
}
Jaydeep parmar
  • 561
  • 2
  • 15
고상현
  • 11
  • 3
  • Have you tried to debug `MediatorResult.Success(endOfPaginationReached = response.body.nextPageToken == null)` case getting valid null case on last page? – pRaNaY Aug 05 '22 at 04:27
  • Thank your answer! Sure i tried. Event if i set false forcely `MediatorResult.Success(endOfPaginationReached = false)`, got same result. I have a doubt because `lazyYoutubeMostPopularVideoList.itemSnapshotList` size is equal to items count precisely. Is there something can disturb `Flow` emit `PageData` intact to UI layer? – 고상현 Aug 05 '22 at 07:23

1 Answers1

0

Unfortunately, I couldn't find exact reasons. I just changed compose LazyColumn to RecyclerView I concreted.
Have no choice but to think there might be some bugs, because I've never touched any other classes above.
Now mediator itself works perfectly.
Sorry for I can't offer solution whose interested with this subject.

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val youtubeMostPopularVideoUseCaseAdapter = YoutubeMostPopularVideoUseCaseAdapter()
        binding.youtubeList.adapter = youtubeMostPopularVideoUseCaseAdapter

        lifecycleScope.launch {
            mainViewModel.youtubeVideoList.collectLatest(
                youtubeMostPopularVideoUseCaseAdapter::submitData
            )
        }
고상현
  • 11
  • 3