Обесценивание пользовательского PageKeyedDataSource заставляет представление представления переработчика - PullRequest
0 голосов
/ 02 апреля 2020

Я пытаюсь реализовать библиотеку подкачки android с пользовательским PageKeyedDataSource. Этот источник данных будет запрашивать данные из базы данных и вставлять объявления случайным образом на этой странице.

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

В чем причина этого ?

Источник данных:

    class ColorsDataSource(
    private val colorsRepository: ColorsRepository
) : PageKeyedDataSource<Int, ColorEntity>() {

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, ColorEntity>
    ) {
        Timber.i("loadInitial()  offset 0 params.requestedLoadSize $params.requestedLoadSize")
        val resultFromDB = colorsRepository.getColors(0, params.requestedLoadSize)
        // TODO insert Ads here
        callback.onResult(resultFromDB, null, 1)
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ColorEntity>) {
        val offset = params.key * params.requestedLoadSize
        Timber.i("loadAfter()    offset $offset params.requestedLoadSize $params.requestedLoadSize")
        val resultFromDB = colorsRepository.getColors(
            offset,
            params.requestedLoadSize
        )
        // TODO insert Ads here
        callback.onResult(resultFromDB, params.key + 1)
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, ColorEntity>) {
        // No- Op
    }
}

BoundaryCallback

class ColorsBoundaryCallback(
    private val colorsRepository: ColorsRepository,
    ioExecutor: Executor,
    private val invalidate: () -> Unit
) : PagedList.BoundaryCallback<ColorEntity>() {

    private val helper = PagingRequestHelper(ioExecutor)

    /**
     * Database returned 0 items. We should query the backend for more items.
     */
    @MainThread
    override fun onZeroItemsLoaded() {
        helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) { pagingRequestHelperCallback ->
            Timber.i("onZeroItemsLoaded() ")
            colorsRepository.colorsApiService.getColorsByCall(
                ColorsRepository.getQueryParams(
                    1,
                    ColorViewModel.PAGE_SIZE
                )
            ).enqueue(object : Callback<List<ColorsModel?>?> {
                override fun onFailure(call: Call<List<ColorsModel?>?>, t: Throwable) {
                    handleFailure(t, pagingRequestHelperCallback)
                }

                override fun onResponse(
                    call: Call<List<ColorsModel?>?>,
                    response: Response<List<ColorsModel?>?>
                ) {
                    handleSuccess(response, pagingRequestHelperCallback)
                }
            })
        }
    }

    private fun handleSuccess(
        response: Response<List<ColorsModel?>?>,
        pagingRequestHelperCallback: PagingRequestHelper.Request.Callback
    ) {
        colorsRepository.saveColorsIntoDb(response.body())
        invalidate.invoke()
        Timber.i("onZeroItemsLoaded() with listOfColors")
        pagingRequestHelperCallback.recordSuccess()
    }

    /**
     * User reached to the end of the list.
     */
    @MainThread
    override fun onItemAtEndLoaded(itemAtEnd: ColorEntity) {
        Timber.i("onItemAtEndLoaded() ")
        helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { pagingRequestHelperCallback ->
            val nextPage = itemAtEnd.nextPage?.toInt() ?: 0
            colorsRepository.colorsApiService.getColorsByCall(
                ColorsRepository.getQueryParams(
                    nextPage,
                    ColorViewModel.PAGE_SIZE
                )
            ).enqueue(object : Callback<List<ColorsModel?>?> {
                override fun onFailure(call: Call<List<ColorsModel?>?>, t: Throwable) {
                    handleFailure(t, pagingRequestHelperCallback)
                }

                override fun onResponse(
                    call: Call<List<ColorsModel?>?>,
                    response: Response<List<ColorsModel?>?>
                ) {
                    handleSuccess(response, pagingRequestHelperCallback)
                }

            })
        }
    }

    private fun handleFailure(
        t: Throwable,
        pagingRequestHelperCallback: PagingRequestHelper.Request.Callback
    ) {
        Timber.e(t)
        pagingRequestHelperCallback.recordFailure(t)
    }
}

DiffUtil адаптера

class DiffUtilCallBack : DiffUtil.ItemCallback<ColorEntity>() {
        override fun areItemsTheSame(oldItem: ColorEntity, newItem: ColorEntity): Boolean {
            return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: ColorEntity, newItem: ColorEntity): Boolean {
            return oldItem.hexString == newItem.hexString
                    && oldItem.name == newItem.name
                    && oldItem.colorId == newItem.colorId
        }
    }

ViewModel

class ColorViewModel(private val repository: ColorsRepository) : ViewModel() {

    fun getColors(): LiveData<PagedList<ColorEntity>> = postsLiveData

    private var postsLiveData: LiveData<PagedList<ColorEntity>>
    lateinit var dataSourceFactory: DataSource.Factory<Int, ColorEntity>
    lateinit var dataSource: ColorsDataSource

    init {
        val config = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setEnablePlaceholders(false)
            .build()

        val builder = initializedPagedListBuilder(config)
        val contentBoundaryCallBack =
            ColorsBoundaryCallback(repository, Executors.newSingleThreadExecutor()) {
                invalidate()
            }
        builder.setBoundaryCallback(contentBoundaryCallBack)
        postsLiveData = builder.build()
    }

    private fun initializedPagedListBuilder(config: PagedList.Config):
            LivePagedListBuilder<Int, ColorEntity> {

        dataSourceFactory = object : DataSource.Factory<Int, ColorEntity>() {
            override fun create(): DataSource<Int, ColorEntity> {
                dataSource =  ColorsDataSource(repository)
                return dataSource
            }
        }
        return LivePagedListBuilder<Int, ColorEntity>(dataSourceFactory, config)
    }

    private fun invalidate() {
        dataSource.invalidate()
    }

    companion object {
        const val PAGE_SIZE = 8
    }
}

Ответы [ 2 ]

1 голос
/ 06 апреля 2020

Каждый раз, когда вызывается invalidate(), весь список будет считаться недействительным и снова будет полностью скомпилирован, создавая новый экземпляр DataSource . На самом деле это ожидаемое поведение, но давайте посмотрим на пошаговую последовательность событий, происходящих под капотом, чтобы понять проблему:

  1. A DataSource экземпляр создан, и его Вызван метод loadInitial с нулевыми элементами (поскольку данные еще не сохранены)
  2. BoundaryCallback 'onZeroItemsLoaded будет вызван, поэтому первые данные будут извлечены, сохранены и наконец, он сделает список недействительным, поэтому он будет создан снова.
  3. Будет создан новый экземпляр DataSource , который снова вызовет loadInitial, но на этот раз, поскольку он уже есть некоторые данные, он будет извлекать эти ранее сохраненные элементы.
  4. Пользователь прокрутит до конца списка, поэтому будет попытаться загрузить новую страницу из DataSource , вызвав loadAfter, который получит 0 элементов, поскольку больше нет элементов для загрузки.
  5. Так будет вызван onItemAtEndLoaded в BoundaryCallback , извлечение второй страницы, сохранение новых элементов и, наконец, недействительность Снова весь список.
  6. Опять же, будет создан новый Источник данных , еще раз вызвав его loadInitial, который будет получать только первые элементы страницы.
  7. После повторного вызова loadAfter он сможет извлекать новые элементы страницы, как только что они были добавлены.
  8. Это будет go для каждой страницы.

Проблему здесь можно определить по Шаг 6 .

Дело в том, что каждый раз, когда мы аннулируем DataSource , его loadInitial будет извлекать только первые элементы страницы. Несмотря на то, что все остальные элементы страниц уже сохранены, новый список не будет знать об их существовании, пока не будет вызван соответствующий им loadAfter. Таким образом, после извлечения новой страницы, сохранения их элементов и аннулирования списка наступит момент, когда новый список будет составлен только из элементов первой страницы (так как loadInitial будет только извлекать их). Этот новый список будет отправлен на Adapter , поэтому RecyclerView будет отображать только первые элементы страницы, создавая впечатление, что он снова перепрыгнул на первый элемент. Однако реальность такова, что все остальные элементы были удалены, так как теоретически их больше нет в списке. После этого, как только пользователь прокрутит вниз, будет вызван соответствующий loadAfter, и элементы страницы будут снова извлечены из сохраненных, пока не будет нажата новая страница без сохраненных элементов, что снова сделает недействительным весь список после хранения новых элементов.

Итак, чтобы избежать этого, хитрость заключается в том, чтобы loadInitial не только всегда извлекал элементы первой страницы, но и все уже загруженные элементы . Таким образом, когда страница становится недействительной и вызывается новый DataSource loadInitial, новый список больше не будет состоять только из первых элементов страницы, а из всех уже загруженных, поэтому что они не удалены из RecyclerView .

. Для этого мы можем отслеживать, сколько страниц уже загружено, чтобы мы могли сообщать каждому новому источникам данных сколько их должно быть получено в loadInitial.


Простое решение состоит в создании класса для отслеживания текущей страницы:

class PageTracker {
    var currentPage = 0
}

Затем измените пользовательский DataSource , чтобы получить экземпляр этого класса, и обновите его:

class ColorsDataSource(
    private val pageTracker: PageTracker
    private val colorsRepository: ColorsRepository
) : PageKeyedDataSource<Int, ColorEntity>() {

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, ColorEntity> 
    ) {
        //...
        val alreadyLoadedItems = (pageTracker.currentPage + 1) * params.requestedLoadSize
        val resultFromDB = colorsRepository.getColors(0, alreadyLoadedItems)
        callback.onResult(resultFromDB, null, pageTracker.currentPage + 1)
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ColorEntity>) {
        pageTracker.currentPage = params.key
        //...
    }

    //...
}

Наконец, создайте экземпляр PageTracker и передайте его каждому новому Источник данных экземпляр

dataSourceFactory = object : DataSource.Factory<Int, ColorEntity>() {

    val pageTracker = PageTracker()

    override fun create(): DataSource<Int, ColorEntity> {
        dataSource =  ColorsDataSource(pageTracker, repository)
        return dataSource
    }
}

ПРИМЕЧАНИЕ 1

Важно отметить, что если необходимо повторно обновить sh весь список снова (из-за действия pull-to-refre sh или чего-либо еще), экземпляр PageTracker перед аннулированием списка потребуется обновить его до currentPage = 0.


ПРИМЕЧАНИЕ 2

Также важно отметить, что этот подход обычно не требуется при использовании Room , так как в этом случае нам, вероятно, не нужно создавать наш пользовательский DataSource , а вместо этого сделать Dao напрямую возвращает DataSource.Factory непосредственно из запроса. Затем, когда мы получим новые данные из-за BoundaryCallback вызовов и сохраняем элементы, Room автоматически обновит наш список с всеми элементами.

0 голосов
/ 10 апреля 2020

В DiffUtilCallback при areItemsTheSame сравнивают идентификаторы вместо ссылок:

override fun areItemsTheSame(oldItem: ColorEntity, newItem: ColorEntity): Boolean 
              = oldItem.db_id == newItem.db_id

Таким образом recyclerView найдет предыдущую позицию из идентификаторов вместо ссылок.

...