Android Paging3最后一页数据项为null问题求助
Paging3 分页异常问题
问题现象
- 分页大小设置为N(示例中为3),系统自动发起t次请求后停止
- 从网络获取的数据已准确存入数据库,总数为N*t条
- LazyPagingItems的快照总数正确,但最后一页的N个数据项为null
- 滚动到LazyColumn底部时,不再调用RemoteMediator的load方法
数据流转流程
Composable(Activity) → ViewModel → UseCase(数据转换) → Repository(Pager) → RemoteMediator
MainActivity(Composable)代码
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 } } }
RemoteMediator代码
@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() }
内容的提问来源于stack exchange,提问作者고상현
相关产品推荐
相关产品推荐

