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()
}