Как перейти к позиции с пейджером просмотра 2, который использует пейджинг 3 для загрузки данных?

Я использую ViewPager2 для отображения данных, которые я получаю с сервера и сохраняю в базе данных Room с помощью библиотеки Paging 3. У меня вопрос: как перейти к конкретному элементу просмотра на пейджер с помощью кода? Если я использую viewPager.setCurrentItem (position, false), это не сработает. Например, если всего 3000 элементов, которые я динамически загружаю, проводя пальцем влево / вправо, как мне установить позицию на 1000, а затем перемещаться / загружать данные в обоих направлениях оттуда? Я не могу заставить это работать.

PS: В приведенном ниже классе DogPagingMediator я также попытался установить некоторый начальный номер в блоке обновления вместо последнего (самого высокого) номера, но при загрузке приложения пейджер представления будет запускаться только в этой позиции, если элементы с более высокими номерами не t существует локально в базе данных, в противном случае он всегда будет начинаться с элемента с наибольшим номером, независимо от страницы, возвращаемой при обновлении (я предполагаю, поскольку dogDao.getDogs () извлекает все элементы в базе данных в порядке убывания).

P.P.S: Причина, по которой я использую живые данные, а не поток, заключается в том, что поток по какой-то причине вызывает исключение NullPointerException, когда я смахиваю.

Код из onCreateView внутри фрагмента, содержащего пейджер представления:

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

Из модели представления:

val pagingDataStream = repository.getAllDogsPagingData()

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

Из репозитория:

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

Посредник (аналогично примеру googles paging3 codelab, за исключением сортировки по убыванию): 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(...)
    }
}

адаптер:

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()
        }
    }
}

person axlrtr    schedule 22.11.2020    source источник
comment
что вы имеете в виду, говоря, что не работает? выдает ошибку или просто не перемещается в позицию?   -  person musooff    schedule 10.04.2021
comment
В позицию не идет, журнала ошибок нет. Когда я устанавливаю позицию viewpager, например, на 100, все, что он делает, это переходит к последней позиции в уже загруженном списке, который был загружен при ручном смахивании, что может быть позицией 15, даже если есть тысячи, которые могут быть загружены. Удаленный посредник тоже не вызывается. Это все еще не работает в rc1 библиотеки подкачки. Я не уверен, поддерживает ли пейджинг пейджер представления, потому что Google не предоставил никаких примеров, но viewpager2 построен на представлении recycler, поэтому я бы подумал, что должен быть способ загрузить и установить позицию.   -  person axlrtr    schedule 02.05.2021


Ответы (2)


В официальной документации сказано:

Установить текущую выбранную страницу. Если ViewPager уже прошел через свой первый макет с текущим адаптером, будет плавный анимированный переход между текущим элементом и указанным элементом. Беззвучно игнорируется, если адаптер не установлен или пуст. Прижимает элемент к границам адаптера.

Перед вызовом этого метода вы должны убедиться, что N-й элемент существует в вашем адаптере.

person Kozmotronik    schedule 06.12.2020
comment
Это не отвечает на вопрос. Вопрос в том, как это сделать с пейджингом 3. - person axlrtr; 06.12.2020
comment
-Мой вопрос в том, как мне перейти к конкретному элементу пейджера просмотра с помощью кода? Если я использую viewPager.setCurrentItem (position, false), это не сработает. Например, если всего 3000 элементов, которые я динамически загружаю при смахивании влево / вправо, как мне установить позицию на 1000, а затем перемещаться / загружать данные в обоих направлениях? - Этот вопрос принадлежит вам правильно? Я не вижу ничего общего с пейджингом 3. - person Kozmotronik; 06.12.2020
comment
Буквально в названии ... - person axlrtr; 10.12.2020

Мне удалось добиться этого, создав свой собственный источник подкачки. Как вы уже упоминали, источник подкачки по умолчанию для дао начнет загружаться с начала таблицы. Поэтому вам необходимо создать источник подкачки, который обрабатывает загрузку данных из базы данных с определенной позиции.

Для этого необходимо реализовать источник подкачки, который запрашивает базу данных со смещением и ограничением:

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 (params is LoadParams.Append && 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
}

Для этого вам также понадобится новый запрос к таблице ваших собак:

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
}

Это работает, только если ваш размер страницы равен 1, но это должно быть так, если вы используете его для просмотра. Если нет, вам нужно немного изменить источник подкачки.

person Rich    schedule 13.07.2021