androidandroid-jetpack-composeandroid-mvvmandroid-pagingandroid-paging-3

Android Compose Paging 3 - Loading all pages at once without scrolling in LazyColumn with network call and no inner scrolls


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.


Solution

  • 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 }