Reputation: 3547
Problem: I get 40 items at the beginning of the list, then it starts to count from 11, and after this, everything is good. So, 1...40,11,12,13,...,300.
And when I scroll a lot to the bottom and then scroll up to see first items, the items have been changed to 1,2,...,10,1,2,...,10,1,2,...,10,11,12,...,300.
But, when I pass false
to enablePlaceholders
in the PagingConfig
, when I scroll to the bottom, I see the issue as I said above(1,2,..,40,11,...,300) and suddenly the 40 items vanish and I only see 1,2,...,10 + 11,12,...,300(the correct way); And it doesn't change or get worse again.
ProductsPagingSource:
@Singleton
class ProductsPagingSource @Inject constructor(
private val productsApi: ProductsApi
//private val query: String
) : RxPagingSource<Int, RecyclerItem>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, RecyclerItem>> {
val position = params.key ?: STARTING_PAGE_INDEX
//val apiQuery = query
return productsApi.getBeersList(position, params.loadSize)
.subscribeOn(Schedulers.io())
.map { listBeerResponse ->
listBeerResponse.map { beerResponse ->
beerResponse.toDomain()
}
}
.map { toLoadResult(it, position) }
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(
@NonNull response: List<RecyclerItem>,
position: Int
): LoadResult<Int, RecyclerItem> {
return LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1,
itemsBefore = LoadResult.Page.COUNT_UNDEFINED,
itemsAfter = LoadResult.Page.COUNT_UNDEFINED
)
}
}
ProductsListRepositoryImpl:
@Singleton
class ProductsListRepositoryImpl @Inject constructor(
private val pagingSource: ProductsPagingSource
) : ProductsListRepository {
override fun getBeers(ids: String): Flowable<PagingData<RecyclerItem>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = true,
maxSize = 30,
prefetchDistance = 5,
initialLoadSize = 40
),
pagingSourceFactory = { pagingSource }
).flowable
}
ProductsListViewModel:
class ProductsListViewModel @ViewModelInject constructor(
private val getBeersUseCase: GetBeersUseCase
) : BaseViewModel() {
private val _ldProductsList: MutableLiveData<PagingData<RecyclerItem>> = MutableLiveData()
val ldProductsList: LiveData<PagingData<RecyclerItem>> = _ldProductsList
init {
loading(true)
getProducts("")
}
private fun getProducts(ids: String) {
loading(false)
getBeersUseCase(GetBeersParams(ids = ids))
.cachedIn(viewModelScope)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
_ldProductsList.value = it
}.addTo(compositeDisposable)
}
}
ProductsListFragment:
@AndroidEntryPoint
class ProductsListFragment : Fragment(R.layout.fragment_product_list) {
private val productsListViewModel: ProductsListViewModel by viewModels()
private val productsListAdapter: ProductsListAdapter by lazy {
ProductsListAdapter(::navigateToProductDetail)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecycler()
setupViewModel()
}
private fun setupRecycler() {
itemErrorContainer.gone()
productListRecyclerView.adapter = productsListAdapter
}
private fun setupViewModel() {
productsListViewModel.run {
observe(ldProductsList, ::addProductsList)
observe(ldLoading, ::loadingUI)
observe(ldFailure, ::handleFailure)
}
}
private fun addProductsList(productsList: PagingData<RecyclerItem>) {
loadingUI(false)
productListRecyclerView.visible()
productsListAdapter.submitData(lifecycle, productsList)
}
...
BASE_DIFF_CALLBACK:
val BASE_DIFF_CALLBACK = object : DiffUtil.ItemCallback<RecyclerItem>() {
override fun areItemsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
return oldItem == newItem
}
}
BasePagedListAdapter:
abstract class BasePagedListAdapter(
vararg types: Cell<RecyclerItem>,
private val onItemClick: (RecyclerItem, ImageView) -> Unit
) : PagingDataAdapter<RecyclerItem, RecyclerView.ViewHolder>(BASE_DIFF_CALLBACK) {
private val cellTypes: CellTypes<RecyclerItem> = CellTypes(*types)
override fun getItemViewType(position: Int): Int {
getItem(position).let {
return cellTypes.of(it).type()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return cellTypes.of(viewType).holder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position).let {
cellTypes.of(it).bind(holder, it, onItemClick)
}
}
}
Upvotes: 2
Views: 3371
Reputation: 21
Your issue is most likely because at the repository layer your results page numbers are indexed with numbers derived from the total divided by your page size.
This page number scheme assumes that you will page through the items with pages of the same size. However, Paging wants to get an initial page that's three times bigger by default, and then each page should be the page size.
So, the indexes might be like 0 through totalElements/PageSize, with the number for page that includes a given position equaling itemsIndex/PageSize.
This part is especially relevant:
return LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1,
itemsBefore = LoadResult.Page.COUNT_UNDEFINED,
itemsAfter = LoadResult.Page.COUNT_UNDEFINED
)
Let's imagine a pagesize of 10, an initial load of 30, and 100 elements total. We start with page 0, and we request 30 items. Position is 0, and params.loadSize is 30.
productsApi.getBeersList(0, 30)
Then we return a page object with those 30 elements and a nextPage key of 1.
If we imagine all our objects as list, the first page would be the asterisks in this span: [***-------]
Let's get the next page: productsApi.getBeersList(1, 10)
That returns elements that are this from our span: [-*--------]
And then you get: [--*-------]
So the 0th page is fine, but the 1th and 2nd page overlap with it. Then, pages 3 and onward contain new elements.
Because you're getting the keys for the next and previous pages by adding or subtracting to get the adjacent keys to the current page of the current length, the index can't scale with the page size. This isn't always easy without knowing what is inside the PagingConfig.
However you can make it work with dynamic page sizes if you can guarantee your initial load size is your regular page size times some integer and that you'll always get the requested number of items except for the last page, you could store an offset as your next/previous page keys, and turn that offset into the next page at load like:
/* Value for api pageNo */
val pageNo = (params.key/params.loadSize + STARTING_PAGE_INDEX) ?: STARTING_PAGE_INDEX
productsApi.getBeersList(pageNo, params.loadSize) // and so on
/* for offset keys in PageData */
nextKey = (pageNo + 1) * loadSize
prevKey = (pageNo - 1) * loadSize // Integer division rounds down so larger windows start where they should
Upvotes: 1
Reputation: 3547
I had to set the same number for pageSize
and initialLoadSize
. Also, I had to set the enablePlaceholders
to false
.
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
maxSize = 30,
prefetchDistance = 5,
initialLoadSize = 10
),
But still, I want to know if it's the normal way? If yes, I couldn't find anywhere point to this! If not, why's that? Why initialLoadSize
can not have value more than the pageSize
?!
As we can see, the default value for the initialLoadSize
is:
internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,
Upvotes: 13