I'm working on an Android Compose project where I'm using Paging 3 along with a LazyColumn to display a list of items fetched from a network call. The default behavior of Paging 3 is to load pages based on prefetchDistance
as the user scrolls, but in my case, it is loading around 15 pages, each containing 10 records on launch of the screen.
I have tried setting the prefetchDistace
to 10, but it doesnt work.
Here is my code: ViewModel
class ShowAllNotificationsViewModel @Inject constructor(private val pagingSource: NotificationPagingSource) :
BaseViewModel() {
private val _uiState =
MutableStateFlow<UIState<BaseResponseModel<NotificationResponseModel?>>>(UIState.StateLoading)
val uiState = _uiState.asStateFlow()
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing = _isRefreshing.asStateFlow()
val items: Flow<PagingData<NotificationModel>> = Pager(
config = PagingConfig(
pageSize = 10,
prefetchDistance = 5,
enablePlaceholders = false,
),
pagingSourceFactory = { pagingSource }
)
.flow
.cachedIn(viewModelScope)
fun refreshData() {
pagingSource.invalidate()
}
}
PagingSource:
class NotificationPagingSource @Inject constructor(val requestNotificationsUseCase: GetNotificationsUseCase) :
PagingSource<String, NotificationModel>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, NotificationModel> {
Timber.e(IllegalStateException())
Timber.d("load params = $params")
val responseModel = requestNotificationsUseCase(params.key)
val result = responseModel?.getAsResult();
result?.let {
return@load when (result) {
is Result.Error<*>,
Result.Loading,
is Result.SessionTimeoutError<*> -> {
LoadResult.Error(
SessionTimeoutException()
)
}
is Result.Success -> {
LoadResult.Page(
result.value.data?.notifications ?: listOf(),
prevKey = null,
nextKey = result.value.data?.lastId,
)
}
}
}
return LoadResult.Error(
IllegalStateException()
)
}
override fun getRefreshKey(state: PagingState<String, NotificationModel>): String? {
Timber.d("getRefreshKey = state = $state")
return null
}
}
UI :
@Composable
fun ShowAllNotificationCompose(items: LazyPagingItems<NotificationModel>) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
Timber.d("ShowAllNotificationCompose : state = ${items.loadState}")
HeaderCompose()
Box(
modifier = Modifier
.weight(1f)
) {
val listState = rememberLazyListState()
LazyColumn(
modifier = Modifier,
state = listState
) {
items(
count = items.itemCount,
key = {
items[it]?.id ?: 0
}
) {
val notificationModel = items[it]
if (notificationModel != null) {
NotificationItem(item = notificationModel)
} else {
NotificationShimmerItem()
}
}
if (items.loadState.append == LoadState.Loading) {
items(2) {
NotificationShimmerItem()
}
}
}
}
}
}
}
Is there a way to modify this setup to ensure that pagination request are invoked only after reaching the prefectchDistance
.
Thanks in advance.
The problem is in your key
:
key = { items[it]?.id ?: 0 }
when you call LazyPagingItems::get
it notifies Paging of the item access which can trigger page load. From the docs:
Returns the presented item at the specified position, notifying Paging of the item access to trigger any loads necessary to fulfill prefetchDistance.
And the key
is called for all the items immediately, not only when they are displayed. So for key, you should use the peek
function.
Also, be very careful with the default key value when the item at index is null
. You cannot have two items with the same key in LazyColumn
, that will crash your app, so 0
is a bad choice. You can for example derive the key from index
. The best option though is to use special itemKey
function provided by the paging library.
key = { index -> items.peek(index)?.id ?: "null at $index" }
key = items.itemKey { it.id }