I am creating an app, where the data will be fetched from an api, cached locally, and provided to my UI using paging 3. The weird thing is, when I reach the bottom of my list, no more items are being loaded.
The logs indicate that the next page (page 2) is called, so I really cannot understand why. I tried changing the pageSize to pageConfig, but to no avail.
@OptIn(ExperimentalPagingApi::class)
class GeneralRemoteMediator(
private val apiService: ApiService,
private val database: GeneralDatabase
) : RemoteMediator<Int, GeneralEntity>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, GeneralEntity>): MediatorResult {
try {
val loadKey = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success(endOfPaginationReached = true)
lastItem.page + 1
}
}
val response = apiService.getPopularItems(loadKey)
database.withTransaction {
if (loadType == LoadType.REFRESH) {
database.generalDao().clearAll()
}
val entities = response.results.map { it.toGeneralEntity(loadKey) }
database.generalDao().insertAll(entities)
}
return MediatorResult.Success(endOfPaginationReached = response.results.isEmpty())
} catch (e: Exception) {
return MediatorResult.Error(e)
}
}
}
class GeneralPagingSource(
private val dao: GeneralDao
) : PagingSource<Int, GeneralEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GeneralEntity> {
try {
val pageNumber = params.key ?: 1
val items = dao.getItemsByPage(pageNumber)
return LoadResult.Page(
data = items,
prevKey = if (pageNumber == 1) null else pageNumber - 1,
nextKey = if (items.isEmpty()) null else pageNumber + 1
)
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, GeneralEntity>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
class GeneralRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val database: GeneralDatabase
) : GeneralRepository {
@OptIn(ExperimentalPagingApi::class)
override fun getItems(): Flow<PagingData<GeneralItem>> {
val pagingConfig = PagingConfig(pageSize = 20, initialLoadSize = 40, enablePlaceholders = false)
return Pager(
config = pagingConfig,
remoteMediator = GeneralRemoteMediator(apiService, database),
pagingSourceFactory = { GeneralPagingSource(database.generalDao()) }
).flow.flowOn(Dispatchers.IO).map { pagingData ->
pagingData.map { it.toGeneralItem() }
}
}
}
class GetAllItemsUseCase @Inject constructor(
private val repository: GeneralRepository
) {
fun getItems(): Flow<PagingData<GeneralItem>> {
return repository.getItems()
}
}
@HiltViewModel
class GeneralViewModel @Inject constructor(
private val getAllItemsUseCase: GetAllItemsUseCase
) : ViewModel() {
val itemsFlow: Flow<PagingData<GeneralItem>> = getAllItemsUseCase.getItems().cachedIn(viewModelScope)
}
@Composable
fun AllItemsScreen(
navController: NavController,
viewModel: AllItemsViewModel = hiltViewModel()
) {
val items = viewModel.itemsFlow.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
items(items.itemCount) { index ->
items[index]?.let { item ->
val imagePainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(data = item.imageUrl ?: item.imageFromLocal)
.build()
)
ItemCard(
itemImagePainter = imagePainter,
itemTitle = item.title,
itemReleaseDate = item.releaseDate,
rating = item.rating.toFloat(),
isFavorite = item.isFavorite,
onFavoriteChange = {
// Implement favorite change logic here
}
)
}
}
items.apply {
when {
loadState.append is LoadState.Loading -> {
item { LoadingItem() }
}
loadState.refresh is LoadState.Error -> {
val e = items.loadState.refresh as LoadState.Error
item { ErrorItem(message = e.error.localizedMessage ?: "Unknown Error", onClickRetry = { retry() }) }
}
loadState.append.endOfPaginationReached -> {
item {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text("You've reached the end of the list")
}
}
}
}
}
}
}
@Composable
fun LoadingItem() {
Box(modifier = Modifier
.fillMaxWidth()
.padding(16.dp), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
@Composable
fun ErrorItem(message: String, onClickRetry: () -> Unit) {
Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = message)
Button(onClick = { onClickRetry() }) {
Text("Retry")
}
}
}
@Composable
fun ItemCard(
itemImagePainter: Painter,
itemTitle: String,
itemReleaseDate: String,
rating: Float,
isFavorite: Boolean,
onFavoriteChange: () -> Unit
) {
// Implementation for item card
}