androidandroid-jetpack-composejetpack-compose-accompanistandroid-jetpack-compose-pager

Horizontal Pager with Paging3 in Jetpack Compose


There are two screens in my app. The first screen shows the list of images on the device by using the Paging3 library in a vertical grid. Now, when the user clicks on an image, I am passing the click position to the second screen where I am using HorizontalPager from Accompanist to show the images in full screen. Both the screens share the same ViewModel to fetch images using Paging3.

The code to show the images in HorizontalPager is shown below.

val images: LazyPagingItems<MediaStoreImage> =
    viewModel.getImages(initialLoadSize = args.currentImagePosition + 1, pageSize = 50)
        .collectAsLazyPagingItems()

val pagerState = rememberPagerState(initialPage = currentImagePosition)

Box(modifier = modifier) {
    HorizontalPager(
        count = images.itemCount,
        state = pagerState,
        itemSpacing = 16.dp
    ) { page ->
        ZoomableImage(
            modifier = modifier,
            imageUri = images[page]?.contentUri
        )
    }
}

Here, currentImagePosition is the index of the image clicked on the first screen. I am setting the initialLoadSize to currentImagePosition + 1 which makes sure that the clicked image to be shown is already fetched by the paging library.

When the second screen is opened, the clicked image is shown in full screen as expected. However, when the user swipes for the next image, instead of loading the next image, the image with the index 50 and so on gets loaded as the user swipes further.

I am not sure what am I missing here. Any help will be appreciated.

Edit: Added ViewModel, Repository & Paging code

ViewModel

fun getImages(initialLoadSize: Int = 50): Flow<PagingData<MediaStoreImage>> {
        return Pager(
            config = PagingConfig(
                pageSize = 50,
                initialLoadSize = initialLoadSize,
                enablePlaceholders = true
            )
        ) {
            repository.getImagesPagingSource()
        }.flow.cachedIn(viewModelScope)
    }

Repository

fun getImagesPagingSource(): PagingSource<Int, MediaStoreImage> {
        return ImagesDataSource { limit, offset ->
            getSinglePageImages(
                limit,
                offset
            )
        }
    }
private fun getSinglePageImages(limit: Int, offset: Int): List<MediaStoreImage> {
        val images = ArrayList<MediaStoreImage>()
        val cursor = getCursor(limit, offset)

        cursor?.use {
            val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val dateModifiedColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED)
            val displayNameColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
            val sizeColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)

            while (it.moveToNext()) {
                val id = it.getLong(idColumn)
                val dateModified =
                    Date(TimeUnit.SECONDS.toMillis(it.getLong(dateModifiedColumn)))
                val dateModifiedString = getFormattedDate(dateModified)
                val displayName = it.getString(displayNameColumn)
                val size = it.getLong(sizeColumn)
                val sizeInMbKb = getFileSize(size)
                val contentUri = ContentUris.withAppendedId(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    id
                )

                images.add(
                    MediaStoreImage(
                        id,
                        displayName,
                        dateModifiedString,
                        contentUri,
                        sizeInMbKb
                    )
                )
            }
        }

        cursor?.close()
        return images
    }
private fun getCursor(limit: Int, offset: Int): Cursor? {
        val projection = arrayOf(
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media.DATE_MODIFIED,
            MediaStore.Images.Media.SIZE
        )

        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val bundle = bundleOf(
                ContentResolver.QUERY_ARG_SQL_SELECTION to "${MediaStore.Images.Media.RELATIVE_PATH} like ? ",
                ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf("%${context.getString(R.string.app_name)}%"),
                ContentResolver.QUERY_ARG_OFFSET to offset,
                ContentResolver.QUERY_ARG_LIMIT to limit,
                ContentResolver.QUERY_ARG_SORT_COLUMNS to arrayOf(MediaStore.Images.Media.DATE_MODIFIED),
                ContentResolver.QUERY_ARG_SORT_DIRECTION to ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
            )

            context.contentResolver.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                projection,
                bundle,
                null
            )
        } else {
            context.contentResolver.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                projection,
                "${MediaStore.Images.Media.DATA} like ? ",
                arrayOf("%${context.getString(R.string.app_name)}%"),
                "${MediaStore.Images.Media.DATE_MODIFIED} DESC LIMIT $limit OFFSET $offset",
                null
            )
        }
    }

Paging Data Source

class ImagesDataSource(private val onFetch: (limit: Int, offset: Int) -> List<MediaStoreImage>) :
    PagingSource<Int, MediaStoreImage>() {

    override fun getRefreshKey(state: PagingState<Int, MediaStoreImage>): Int? {
        return state.anchorPosition?.let {
            state.closestPageToPosition(it)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MediaStoreImage> {
        val pageNumber = params.key ?: 0
        val pageSize = params.loadSize
        val images = onFetch.invoke(pageSize, pageNumber * pageSize)
        val prevKey = if (pageNumber > 0) pageNumber.minus(1) else null
        val nextKey = if (images.isNotEmpty()) pageNumber.plus(1) else null

        return LoadResult.Page(
            data = images,
            prevKey = prevKey,
            nextKey = nextKey
        )
    }
}

Solution

  • The issue is solved. Whenever the second screen was opened, the getImages() function of the shared ViewModel was called which created a new instance of Pager. This new instance of the Pager was different from the one used on the first screen. Referring to this answer on StackOverflow, I created the Pager in the init block of the ViewModel as shown below.

        val images: Flow<PagingData<MediaStoreImage>>
    
        init {
            images = Pager(
                config = PagingConfig(
                    pageSize = 50,
                    initialLoadSize = 50,
                    enablePlaceholders = true
                )
            ) {
                repository.getImagesPagingSource()
            }.flow.cachedIn(viewModelScope)
        }
    

    On the second screen, I collected the paging items as shown below.

    val lazyImages: LazyPagingItems<MediaStoreImage> = viewModel.images.collectAsLazyPagingItems()
    

    Now, I didn't have to pass the initialLoadSize as the Pager created in the first screen was being reused which had already loaded the items.