How to go to a position with view pager 2 that uses paging 3 to load data?
Asked Answered
U

2

9

I am using ViewPager2 to display data that I fetch from a server and save to a Room database using the Paging 3 library. My question is, how do I navigate to a specific view pager item through code? If I use viewPager.setCurrentItem(position, false) then this does not work. For example if there are 3000 items total which I dynamically load while swiping left/right, how do I set the position to 1000 and then navigate/load data in both directions from there? I cannot get this to work.

P.S: In the DogPagingMediator class below, I have also tried to set some starting number in the refresh block instead of the latest(highest) number, but when loading the app the view pager will only start at this position if higher numbered items don't exist locally in the database, otherwise it will always start at the highest numbered item regardless of the page returned in refresh(I assume since dogDao.getDogs() fetches all items in the database in descending order).

P.P.S: The reason why I am using live data and not flow is because flow for some reason causes NullPointerException when I swipe.

Code from onCreateView within the fragment containing the view pager:

    lifecycleScope.launch {
        // Fetch the latest dog item from the network (data is sorted by descending)
        if (!browseDogsViewModel.latestDogIsFetched()) {
            browseDogsViewModel.setLatestDogNumber()
        }

        browseDogsViewModel.pagingDataStream.observe(viewLifecycleOwner) {
            adapter.submitData(viewLifecycleOwner.lifecycle, it)
        }
    }

From the view model:

val pagingDataStream = repository.getAllDogsPagingData()

suspend fun setLatestDogNumber() {
    latestDogNumber = repository.getLatestDogNumber()
}

From the repository:

fun getAllDogsPagingData() = Pager(
    config = PagingConfig(pageSize = PAGE_SIZE),
    remoteMediator = dogPagingMediator,
    pagingSourceFactory = { dogDao.getDogs() }
).liveData

The Mediator (similar to googles paging3 codelab example except it sorts by descending): https://codelabs.developers.google.com/codelabs/android-paging/#0):

@OptIn(ExperimentalPagingApi::class)
class DogPagingMediator @Inject constructor(
    private val dogDatabase: DogDatabase,
    private val dogDao: DogDao,
    private val remoteKeysDao: RemoteKeysDao,
    private val service: DogService,
) : RemoteMediator<Int, Dog>() {
    override suspend fun load(loadType: LoadType, state: PagingState<Int, Dog>): MediatorResult {
        try {
            val page = when (loadType) {
                LoadType.REFRESH -> {
                    val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
                    remoteKeys?.nextKey?.plus(PAGE_SIZE) ?: BrowseDogsViewModel.latestDogNumber
                }
                LoadType.PREPEND -> {
                    val remoteKeys = getRemoteKeyForFirstItem(state)
                    if (remoteKeys == null) {
                        // The LoadType is PREPEND so some data was loaded before,
                        // so we should have been able to get remote keys
                        // If the remoteKeys are null, then we're an invalid state and we have a bug
                        throw InvalidObjectException("Remote key and the prevKey should not be null")
                    }
                    // If the previous key is null, then we can't request more data
                    remoteKeys.prevKey
                        ?: return MediatorResult.Success(endOfPaginationReached = true)
                    remoteKeys.prevKey
                }
                LoadType.APPEND -> {
                    val remoteKeys = getRemoteKeyForLastItem(state)
                    if (remoteKeys?.nextKey == null) {
                        throw InvalidObjectException("Remote key should not be null for $loadType")
                    }
                    remoteKeys.nextKey
                }
            }

            val dogs: MutableList<Dog> = mutableListOf()
            for (i in page downTo page - PAGE_SIZE) {
                try {
                    val response = service.geDogWithNumber(i)
                    dogs.add(convertFromDto(response))
                } catch (ex: HttpException) {
                    // Will be 404 when requesting a dog out of range
                    if (ex.code() != 404) {
                    throw ex
                    }
                }
            }

            val endOfPaginationReached = dogs.isEmpty()

            dogDatabase.withTransaction {
                val prevKey =
                    if (page == BrowseDogsViewModel.latestDogNumber) null else page + PAGE_SIZE
                val nextKey = if (endOfPaginationReached) null else page - PAGE_SIZE
                val keys = dogs.map {
                    RemoteKeys(dogNum = it.number, prevKey = prevKey, nextKey = nextKey)
                }

                remoteKeysDao.insertAll(keys)
                dogDao.insertAll(dogs)
            }

            return MediatorResult.Success(
                endOfPaginationReached = endOfPaginationReached
            )
        } catch (exception: IOException) {
            return MediatorResult.Error(exception)
        } catch (exception: HttpException) {
            return MediatorResult.Error(exception)
        }
    }

    private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Dog>): RemoteKeys? {
        // Get the last page that was retrieved, that contained items.
        // From that last page, get the last item
        return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
            ?.let { dog->
                // Get the remote keys of the last item retrieved
                remoteKeysDao.remoteKeysDogNum(dog.number)
            }
    }

    private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Dog>): RemoteKeys? {
        // Get the first page that was retrieved, that contained items.
        // From that first page, get the first item
        return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
            ?.let { dog->
                // Get the remote keys of the first items retrieved
                remoteKeysDao.remoteKeysDogNum(dog.number)
            }
    }

    private suspend fun getRemoteKeyClosestToCurrentPosition(
        state: PagingState<Int, Dog>
    ): RemoteKeys? {
        // The paging library is trying to load data after the anchor position
        // Get the item closest to the anchor position
        return state.anchorPosition?.let { position ->
            state.closestItemToPosition(position)?.number?.let { num ->
                remoteKeysDao.remoteKeysDogNum(num)
            }
        }
    }

    private fun convertFromDto(dogDto: DogDto): Dog {
        return Dog(...)
    }
}

adapter:

class DogPagingAdapter() :
    PagingDataAdapter<Dog, DogPagingAdapter.ViewPagerViewHolder>(DogDiffUtilCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerViewHolder {
        return ViewPagerViewHolder(
            ItemDogViewPagerBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(holder: ViewPagerViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    inner class ViewPagerViewHolder(private val binding: ItemDogViewPagerBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(dog: Dog?) {
            binding.dog = dog
            binding.executePendingBindings()
        }
    }
}
Umont answered 22/11, 2020 at 20:0 Comment(6)
what do you mean by not working? does it give any error or it just doesn't move to the position?Swinney
It doesn't go to the position, there is no error log. When I set the viewpager position to be for example 100, all it does is go to the last position in the already loaded list, which was loaded when manually swiping, which could be position 15, even though there are thousands that can be loaded. The remote mediator isn't called either. This still doesn't work in rc1 of paging library. I am not sure if paging even supports view pager because google hasn't provided any examples, but viewpager2 is built on recycler view so I would think that there should be a way to load and set a position.Umont
Did you solve the problem?? I am having same problem too. I guess the reason is basically every you navigate somewhere, views you not seeing will be destoryed. Your question for "viewPager.setCurrentItem(position, false) then this does not work.", it might not work because you call the method before view, which was last seen position of items, is created. You could use postDelayed() to check. but there is some lags while creating rest views..so I am still finding best solution. If you had solved this problem, please let me knowCzar
Any solution found for this? Stuck in similar situationMann
To answer the question from you both I have still not solved this. Last thing I did was that I tried the solution by Rich which helped but still I had problems which I described as a comment to that answer. In addition I think loading only 1 item at a time kind of defeats the purpose of the paging library. My guess is Google haven't really made this work with view pager 2 because I can't find any examples.Umont
Facing this problem in 2024, any valid solutions?Coriolanus
J
0

The official documentation says:

Set the currently selected page. If the ViewPager has already been through its first layout with its current adapter there will be a smooth animated transition between the current item and the specified item. Silently ignored if the adapter is not set or empty. Clamps item to the bounds of the adapter.

Before you call this method you must ensure that Nth item exists in your adapter.

Johnathon answered 6/12, 2020 at 14:59 Comment(2)
This doesn't answer the question. The question is how to do this with paging 3.Umont
-My question is, how do I navigate to a specific view pager item through code? If I use viewPager.setCurrentItem(position, false) then this does not work. For example if there are 3000 items total which I dynamically load while swiping left/right, how do I set the position to 1000 and then navigate/load data in both directions from there?- This question belongs to you right? I don' see anything to do with paging 3.Johnathon
R
0

I managed to achieve this by creating my own paging source. As you already mentioned, the default paging source of a dao will start loading at the beginning of the table. Therefore you have to create a paging source which handles loading data from the database from a specific position.

To do that have to implement a paging source that queries the database with an offset and limit:

PagingSource

class DogsPagingSource(
    private val database: DogDatabase,
    private val dogDao: DogDao,
    private var startPos: Int
) : PagingSource<Int, Dog>() {

    // to show new loaded data, you have to invalidate the paging source after data in db changed
    init {
        val tableObserver = object : InvalidationTracker.Observer("dogs") {
            override fun onInvalidated(tables: MutableSet<String>) {
                invalidate()
            }
        }
        database.invalidationTracker.addObserver(tableObserver)
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Dog> {
        return try {
            // params.key is null when paging source gets loaded the first time
            // for this case use your start position = number of rows that should be skipped in your table (offset)
            val position =
                if (params.key == null) startPos
                else maxOf(0, params.key ?: 0)

            // load one item from your db by using a limit-offset-query
            // limit is your page size which has to be 1 to get this working
            val dogs = dogDao.getDogsPage(params.loadSize, position)

            // to load further or previous data just in-/decrease position by 1
            // if you are at the start/end of the table set prev-/nextKey to null to notify RemoteMediator to load new data
            // nextKey = null will call APPEND, prevKey = null will call PREPEND in your RemoteMediator
            Page(
                data = dogs,
                prevKey = if (position == 0) null else position.minus(1),
                nextKey = if (position == dogDao.count()) null
                else position.plus(1),
            )
        } catch (e: IOException) {
            LoadResult.Error(e)
        } catch (e: HttpException) {
        LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Item>): Int? =
        state.anchorPosition?.let {
            state.closestPageToPosition(it)?.prevKey?.minus(1)?.minValue(0)
        }

    override val jumpingSupported: Boolean
        get() = true
}

For this you also will need a new query for your dogs table:

DAO

@Dao
interface DogDao {

    ...
    
    // just define the order which the data should be arranged by. limit and offset will do the rest
    @Query("SELECT * FROM `dogs` ORDER BY id ASC LIMIT :limit OFFSET :offset")
    suspend fun getDogsPage(limit: Int, offset: Int): List<Dog>

    @Query("SELECT COUNT(*) FROM `dogs`")
    suspend fun count(): Int
}

This only works if your page size is 1, but this should be the case if you use it for a viewpager. If not, you have to modify the paging source a little bit.

Rafaellle answered 13/7, 2021 at 6:25 Comment(5)
This didn't work for me. The PagingSource load method is never called when navigating back. Also, params.loadSize is 3, even though the page size is 1, so limit would be 3, not 1. I can go to a specific position when setting viewpager position in code, but only if I first insert that item in the database, which is fine but then there are 2 problems: navigating forward does not call PagingSource or mediator so it does nothing, and navigating back goes to the previous item in the database. If I have loaded 2 pages and load item number 1000, navigating back should load number 999, not 2.Umont
You also have to set the initialLoadSize to 1. By default it is pageSize * 3 (see PagingConfig documentation)Rafaellle
Did this and it solved the load size 3 problem, but still when navigating backwards in existing items neither the paging source or the mediator gets called, so if I go to position 1000 and navigate back I'm back at page 2. Also still the problem with pages often automatically navigating back after navigating forward, and I can see that the paging source is called multiple times.Umont
@Rafaellle can you please see me issueMoniliform
Facing the same problem. Have you solved it yet?Oversoul

© 2022 - 2024 — McMap. All rights reserved.